├── .editorconfig ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── auth.go ├── connack.go ├── connack_test.go ├── connect.go ├── connect_test.go ├── disconnect.go ├── example_client_test.go ├── example_server_test.go ├── go.mod ├── go.sum ├── header.go ├── header_test.go ├── mqtt.go ├── packet.go ├── pingreq.go ├── pingresp.go ├── properties.go ├── properties_test.go ├── puback.go ├── pubcomp.go ├── publish.go ├── publish_test.go ├── pubrec.go ├── pubrel.go ├── reader.go ├── readwrite_test.go ├── reason_code.go ├── replies.go ├── suback.go ├── subscribe.go ├── unsuback.go ├── unsubscribe.go └── writer.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | max_line_length = 300 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | charset = utf-8 11 | 12 | [*.go] 13 | indent_style = tab 14 | indent_size = 4 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @htdvisser 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | _Contributor Covenant Code of Conduct_ 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to making participation in our project and 9 | our community a harassment-free experience for everyone, regardless of age, body 10 | size, disability, ethnicity, sex characteristics, gender identity and expression, 11 | level of experience, education, socio-economic status, nationality, personal 12 | appearance, race, religion, or sexual identity and orientation. 13 | 14 | ## Our Standards 15 | 16 | Examples of behavior that contributes to creating a positive environment 17 | include: 18 | 19 | * Using welcoming and inclusive language 20 | * Being respectful of differing viewpoints and experiences 21 | * Gracefully accepting constructive criticism 22 | * Focusing on what is best for the community 23 | * Showing empathy towards other community members 24 | 25 | Examples of unacceptable behavior by participants include: 26 | 27 | * The use of sexualized language or imagery and unwelcome sexual attention or 28 | advances 29 | * Trolling, insulting/derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or electronic 32 | address, without explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies within all project spaces, and it also applies when 51 | an individual is representing the project or its community in public spaces. 52 | Examples of representing a project or community include using an official 53 | project e-mail address, posting via an official social media account, or acting 54 | as an appointed representative at an online or offline event. Representation of 55 | a project may be further defined and clarified by project maintainers. 56 | 57 | ## Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project lead at github (at) htdvisser (dot) nl. All 61 | complaints will be reviewed and investigated and will result in a response that 62 | is deemed necessary and appropriate to the circumstances. The project team is 63 | obligated to maintain confidentiality with regard to the reporter of an incident. 64 | Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ## Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 73 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 74 | 75 | [homepage]: https://www.contributor-covenant.org 76 | 77 | For answers to common questions about this code of conduct, see 78 | https://www.contributor-covenant.org/faq 79 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for contributing to this project! 4 | 5 | ## Issues and Bugs 6 | 7 | If you've found an issue or bug in this project, please check the list of issues 8 | and pull requests to see if there is already an issue or pull request for this. 9 | 10 | If not, you can submit a new issue or pull request. Please follow the issue and 11 | pull request templates, and give as many details as possible. 12 | 13 | ## New Features 14 | 15 | New features for this project can be requested by submitting an issue or pull 16 | request. Major features should ideally be discussed in issues before starting 17 | the implementation. 18 | 19 | ## Code Style 20 | 21 | Code style is mostly specified by an editorconfig file. Please make sure that 22 | your editor either supports [EditorConfig](https://editorconfig.org/) or install 23 | a plugin for it. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What is the current situation? 2 | 3 | 7 | 8 | ### What do you want instead? 9 | 10 | 13 | 14 | ### What does your environment look like? 15 | 16 | 19 | 20 | ### Can you help with this? 21 | 22 | 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What is changing? 2 | 3 | 6 | 7 | ### Why is it changing? 8 | 9 | 12 | 13 | ### How to review these changes? 14 | 15 | 18 | 19 | ### Confirm that this Pull Request is... 20 | 21 | - [ ] **Well Scoped:** it does one thing. 22 | - [ ] **Compatible:** it introduces no breaking changes (to API, existing databases, etc.). 23 | - [ ] **Tested:** the changes are covered with unit tests. 24 | - [ ] **Documented:** the changes are documented with inline comments (godoc). 25 | 26 | ### Developer Certificate of Origin 27 | 28 | ``` 29 | By making a contribution to this project, I certify that: 30 | 31 | (a) The contribution was created in whole or in part by me and I 32 | have the right to submit it under the open source license 33 | indicated in the file; or 34 | 35 | (b) The contribution is based upon previous work that, to the best 36 | of my knowledge, is covered under an appropriate open source 37 | license and I have the right under that license to submit that 38 | work with modifications, whether created in whole or in part 39 | by me, under the same open source license (unless I am 40 | permitted to submit under a different license), as indicated 41 | in the file; or 42 | 43 | (c) The contribution was provided directly to me by some other 44 | person who certified (a), (b) or (c) and I have not modified 45 | it. 46 | 47 | (d) I understand and agree that this project and the contribution 48 | are public and that a record of the contribution (including all 49 | personal information I submit with it, including my sign-off) is 50 | maintained indefinitely and may be redistributed consistent with 51 | this project or the open source license(s) involved. 52 | ``` 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,macos,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=go,macos,visualstudiocode 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | ### Go Patch ### 20 | /vendor/ 21 | /Godeps/ 22 | 23 | ### macOS ### 24 | # General 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must end with two \r 30 | Icon 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # Files that might appear in the root of a volume 36 | .DocumentRevisions-V100 37 | .fseventsd 38 | .Spotlight-V100 39 | .TemporaryItems 40 | .Trashes 41 | .VolumeIcon.icns 42 | .com.apple.timemachine.donotpresent 43 | 44 | # Directories potentially created on remote AFP share 45 | .AppleDB 46 | .AppleDesktop 47 | Network Trash Folder 48 | Temporary Items 49 | .apdisk 50 | 51 | ### VisualStudioCode ### 52 | .vscode/* 53 | !.vscode/settings.json 54 | !.vscode/tasks.json 55 | !.vscode/launch.json 56 | !.vscode/extensions.json 57 | 58 | ### VisualStudioCode Patch ### 59 | # Ignore all local history of files 60 | .history 61 | 62 | # End of https://www.gitignore.io/api/go,macos,visualstudiocode 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQTT 2 | 3 | [![Apache-2.0 License](https://img.shields.io/github/license/htdvisser/mqtt.svg?style=flat-square)](https://github.com/htdvisser/mqtt/blob/master/LICENSE) [![GitHub stars](https://img.shields.io/github/stars/htdvisser/mqtt.svg?logo=github&style=flat-square)](https://github.com/htdvisser/mqtt/stargazers) [![GitHub forks](https://img.shields.io/github/forks/htdvisser/mqtt.svg?logo=github&style=flat-square)](https://github.com/htdvisser/mqtt/network/members) [![GitHub release](https://img.shields.io/github/release/htdvisser/mqtt.svg?logo=github&style=flat-square)](https://github.com/htdvisser/mqtt/releases) [![Go reference](https://img.shields.io/badge/go-reference-blue?style=flat-square)](https://pkg.go.dev/htdvisser.dev/mqtt) 4 | 5 | > Package `htdvisser.dev/mqtt` implements MQTT 3.1.1 and MQTT 5.0 packet types as well as a reader and a writer. 6 | 7 | ## Background 8 | 9 | MQTT is a publish/subscribe messaging transport protocol often used in Internet of Things communication. The MQTT specifications are maintained by [OASIS](https://www.oasis-open.org/). 10 | 11 | ## Goals and Non-Goals 12 | 13 | The goal of this library is to provide basic MQTT packet types, as well as implementations for reading and writing those packets. This library aims to implement version [3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html) and version [5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html) of the specification, with limited support for version 3.1. 14 | 15 | This library does not -- and will not -- implement a client or server (broker), but it can be used by client or server implementations. 16 | 17 | ## Install 18 | 19 | ```sh 20 | go get -u htdvisser.dev/mqtt 21 | ``` 22 | 23 | ## Usage 24 | 25 | See the examples on [pkg.go.dev](https://pkg.go.dev/htdvisser.dev/mqtt). 26 | 27 | ## Contributing 28 | 29 | See [CONTRIBUTING.md](.github/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md). 30 | 31 | ## License 32 | 33 | [Apache-2.0](LICENSE) © Hylke Visser 34 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // AuthPacket is the Auth packet. 4 | type AuthPacket struct { 5 | AuthHeader 6 | Properties 7 | } 8 | 9 | func (*AuthPacket) _isPacket() {} 10 | 11 | // PacketType returns the packet type of the Auth packet. 12 | func (AuthPacket) PacketType() PacketType { return AUTH } 13 | 14 | func (p AuthPacket) fixedHeader(protocol byte) (h FixedHeader) { 15 | h.SetPacketType(AUTH) 16 | h.remainingLength = p.size(protocol) 17 | return 18 | } 19 | 20 | func (p AuthPacket) size(protocol byte) uint32 { 21 | size := 0 22 | if protocol >= 5 { 23 | size++ 24 | size += int(p.Properties.size()) 25 | } 26 | return uint32(size) 27 | } 28 | 29 | // AuthHeader is the header of the Auth packet. 30 | type AuthHeader struct { 31 | ReasonCode 32 | } 33 | 34 | func (r *PacketReader) readAuthHeader() { 35 | packet := r.packet.(*AuthPacket) 36 | if r.protocol >= 5 { 37 | var f byte 38 | if f, r.err = r.readByte(); r.err != nil { 39 | return 40 | } 41 | packet.AuthHeader.ReasonCode = ReasonCode(f) 42 | } 43 | } 44 | 45 | func (w *PacketWriter) writeAuthHeader() { 46 | packet := w.packet.(*AuthPacket) 47 | if w.protocol >= 5 { 48 | w.err = w.writeByte(byte(packet.AuthHeader.ReasonCode)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /connack.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // ConnackPacket is the Connack packet. 4 | type ConnackPacket struct { 5 | ConnackHeader 6 | Properties 7 | } 8 | 9 | func (*ConnackPacket) _isPacket() {} 10 | 11 | // PacketType returns the packet type of the Connack packet. 12 | func (ConnackPacket) PacketType() PacketType { return CONNACK } 13 | 14 | func (p ConnackPacket) fixedHeader(protocol byte) (h FixedHeader) { 15 | h.SetPacketType(CONNACK) 16 | h.remainingLength = p.size(protocol) 17 | return 18 | } 19 | 20 | func (p ConnackPacket) size(protocol byte) uint32 { 21 | size := 2 22 | if protocol >= 5 { 23 | size += int(p.Properties.size()) 24 | } 25 | return uint32(size) 26 | } 27 | 28 | // ConnackHeader is the header of the Connack packet. 29 | type ConnackHeader struct { 30 | ConnackHeaderFlags 31 | ReasonCode 32 | } 33 | 34 | // ConnackHeaderFlags are the flags in the header of the Connack packet. 35 | type ConnackHeaderFlags byte 36 | 37 | var errInvalidConnackHeaderFlags = NewReasonCodeError(ProtocolError, "mqtt: invalid connack header flags") 38 | 39 | func (r *PacketReader) validateConnackHeaderFlags(f ConnackHeaderFlags) error { 40 | if r.protocol < 4 && f != 0x00 { 41 | return errInvalidConnackHeaderFlags 42 | } 43 | if f&0xFE != 0x00 { 44 | return errInvalidConnackHeaderFlags 45 | } 46 | return nil 47 | } 48 | 49 | // SessionPresent returns Session Present bit from the connack header flags. 50 | func (f ConnackHeaderFlags) SessionPresent() bool { return f&0x01 == 0x01 } 51 | 52 | // SetSessionPresent sets the Session Present bit into the connack header flags. 53 | func (f *ConnackHeaderFlags) SetSessionPresent(sessionPresent bool) { 54 | *f &^= 0x01 55 | if sessionPresent { 56 | *f |= 0x01 57 | } 58 | } 59 | 60 | func (r *PacketReader) readConnackHeader() { 61 | packet := r.packet.(*ConnackPacket) 62 | var f byte 63 | if f, r.err = r.readByte(); r.err != nil { 64 | return 65 | } 66 | packet.ConnackHeader.ConnackHeaderFlags = ConnackHeaderFlags(f) 67 | if r.err = r.validateConnackHeaderFlags(packet.ConnackHeader.ConnackHeaderFlags); r.err != nil { 68 | return 69 | } 70 | if f, r.err = r.readByte(); r.err != nil { 71 | return 72 | } 73 | packet.ConnackHeader.ReasonCode = ReasonCode(f) 74 | } 75 | 76 | func (w *PacketWriter) writeConnackHeader() { 77 | packet := w.packet.(*ConnackPacket) 78 | var flags byte 79 | if w.protocol >= 4 { 80 | flags = byte(packet.ConnackHeader.ConnackHeaderFlags) 81 | } 82 | if w.err = w.writeByte(flags); w.err != nil { 83 | return 84 | } 85 | w.err = w.writeByte(byte(packet.ConnackHeader.ReasonCode)) 86 | } 87 | -------------------------------------------------------------------------------- /connack_test.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateConnackHeaderFlags(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | r := PacketReader{protocol: 4} 13 | 14 | tests := []struct { 15 | flags ConnackHeaderFlags 16 | valid bool 17 | }{ 18 | {0x01, true}, 19 | {0x02, false}, 20 | } 21 | 22 | for _, test := range tests { 23 | err := r.validateConnackHeaderFlags(test.flags) 24 | if test.valid { 25 | assert.NoError(err) 26 | } else { 27 | assert.Error(err) 28 | } 29 | } 30 | } 31 | 32 | func TestConnackHeaderFlags(t *testing.T) { 33 | assert := assert.New(t) 34 | 35 | var f ConnackHeaderFlags 36 | 37 | assert.False(f.SessionPresent()) 38 | 39 | f.SetSessionPresent(true) 40 | 41 | assert.True(f.SessionPresent()) 42 | 43 | f.SetSessionPresent(false) 44 | 45 | assert.False(f.SessionPresent()) 46 | } 47 | -------------------------------------------------------------------------------- /connect.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // ConnectPacket is the Connect packet. 8 | type ConnectPacket struct { 9 | ConnectHeader 10 | ConnectPayload 11 | Properties 12 | } 13 | 14 | func (*ConnectPacket) _isPacket() {} 15 | 16 | // PacketType returns the packet type of the Connect packet. 17 | func (ConnectPacket) PacketType() PacketType { return CONNECT } 18 | 19 | func (p ConnectPacket) fixedHeader(protocol byte) (h FixedHeader) { 20 | h.SetPacketType(CONNECT) 21 | h.remainingLength = p.size(protocol) 22 | return 23 | } 24 | 25 | func (p ConnectPacket) size(protocol byte) uint32 { 26 | size := 2 + len(p.ConnectHeader.ProtocolName) + 1 + 1 + 2 27 | size += 2 + len(p.ConnectPayload.ClientIdentifier) 28 | if p.ConnectHeader.Will() { 29 | size += 2 + len(p.ConnectPayload.WillTopic) 30 | size += 2 + len(p.ConnectPayload.WillMessage) 31 | if protocol >= 5 { 32 | size += int(p.ConnectPayload.WillProperties.size()) 33 | } 34 | } 35 | if p.ConnectHeader.Username() { 36 | size += 2 + len(p.ConnectPayload.Username) 37 | } 38 | if p.ConnectHeader.Password() { 39 | size += 2 + len(p.ConnectPayload.Password) 40 | } 41 | if protocol >= 5 { 42 | size += int(p.Properties.size()) 43 | } 44 | return uint32(size) 45 | } 46 | 47 | // Username returns the Username from the packet or nil if it doesn't have one. 48 | func (p ConnectPacket) Username() []byte { 49 | if p.ConnectHeader.Username() { 50 | return p.ConnectPayload.Username 51 | } 52 | return nil 53 | } 54 | 55 | // SetUsername sets the Username into the packet if non-nil, or unsets it if nil. 56 | func (p *ConnectPacket) SetUsername(username []byte) { 57 | p.ConnectHeader.SetUsername(username != nil) 58 | p.ConnectPayload.Username = username 59 | } 60 | 61 | // Password returns the Password from the packet or nil if it doesn't have one. 62 | func (p ConnectPacket) Password() []byte { 63 | if p.ConnectHeader.Password() { 64 | return p.ConnectPayload.Password 65 | } 66 | return nil 67 | } 68 | 69 | // SetPassword sets the Password into the packet if non-nil, or unsets it if nil. 70 | func (p *ConnectPacket) SetPassword(password []byte) { 71 | p.ConnectHeader.SetPassword(password != nil) 72 | p.ConnectPayload.Password = password 73 | } 74 | 75 | // Will returns the Will from the packet or nil if it doesn't have one. 76 | func (p *ConnectPacket) Will() (properties Properties, topic, message []byte) { 77 | if p.ConnectHeader.Will() { 78 | return p.ConnectPayload.WillProperties, p.ConnectPayload.WillTopic, p.ConnectPayload.WillMessage 79 | } 80 | return nil, nil, nil 81 | } 82 | 83 | // SetWill sets the Will into the packet if the topic is non-nil, or unsets it if nil. 84 | func (p *ConnectPacket) SetWill(properties Properties, topic, message []byte) { 85 | p.ConnectHeader.SetWill(topic != nil) 86 | p.ConnectPayload.WillProperties = properties 87 | p.ConnectPayload.WillTopic = topic 88 | p.ConnectPayload.WillMessage = message 89 | } 90 | 91 | // ConnectHeader is the header of the Connect packet. 92 | type ConnectHeader struct { 93 | ProtocolName []byte 94 | ProtocolVersion byte 95 | ConnectHeaderFlags 96 | KeepAlive uint16 97 | } 98 | 99 | // ConnectHeaderFlags are the flags in the header of the Connect packet. 100 | type ConnectHeaderFlags byte 101 | 102 | var errInvalidConnectHeaderFlags = NewReasonCodeError(MalformedPacket, "mqtt: invalid connect header flags") 103 | 104 | func (r *PacketReader) validateConnectHeaderFlags(f ConnectHeaderFlags) error { 105 | if f&0x18 == 0x18 { 106 | return errInvalidQoS 107 | } 108 | if f&0x01 == 0x01 { 109 | return errInvalidConnectHeaderFlags 110 | } 111 | return nil 112 | } 113 | 114 | // Username returns the Username bit from the connect header flags. 115 | func (f ConnectHeaderFlags) Username() bool { return f&0x80 == 0x80 } 116 | 117 | // SetUsername sets the Username bit into the connect header flags. 118 | func (f *ConnectHeaderFlags) SetUsername(username bool) { 119 | *f &^= 0x80 120 | if username { 121 | *f |= 0x80 122 | } 123 | } 124 | 125 | // Password returns the Password bit from the connect header flags. 126 | func (f ConnectHeaderFlags) Password() bool { return f&0x40 == 0x40 } 127 | 128 | // SetPassword sets the Password bit into the connect header flags. 129 | func (f *ConnectHeaderFlags) SetPassword(password bool) { 130 | *f &^= 0x40 131 | if password { 132 | *f |= 0x40 133 | } 134 | } 135 | 136 | // WillRetain returns the WillRetain bit from the connect header flags. 137 | func (f ConnectHeaderFlags) WillRetain() bool { return f&0x20 == 0x20 } 138 | 139 | // SetWillRetain sets the WillRetain bit into the connect header flags. 140 | func (f *ConnectHeaderFlags) SetWillRetain(willRetain bool) { 141 | *f &^= 0x20 142 | if willRetain { 143 | *f |= 0x20 144 | } 145 | } 146 | 147 | // Will returns the Will bit from the connect header flags. 148 | func (f ConnectHeaderFlags) Will() bool { return f&0x04 == 0x04 } 149 | 150 | // SetWill sets the Will bit into the connect header flags. 151 | func (f *ConnectHeaderFlags) SetWill(will bool) { 152 | *f &^= 0x04 153 | if will { 154 | *f |= 0x04 155 | } 156 | } 157 | 158 | // CleanStart returns the CleanStart bit from the connect header flags. 159 | func (f ConnectHeaderFlags) CleanStart() bool { return f&0x02 == 0x02 } 160 | 161 | // CleanSession is an alias for CleanStart. 162 | func (f ConnectHeaderFlags) CleanSession() bool { 163 | return f.CleanStart() 164 | } 165 | 166 | // SetCleanStart sets the CleanStart bit into the connect header flags. 167 | func (f *ConnectHeaderFlags) SetCleanStart(cleanSession bool) { 168 | *f &^= 0x02 169 | if cleanSession { 170 | *f |= 0x02 171 | } 172 | } 173 | 174 | // SetCleanSession is an alias for SetCleanStart. 175 | func (f *ConnectHeaderFlags) SetCleanSession(cleanSession bool) { 176 | f.SetCleanStart(cleanSession) 177 | } 178 | 179 | // WillQoS returns the WillQoS from the connect header flags. 180 | func (f ConnectHeaderFlags) WillQoS() QoS { return QoS(f >> 3 & 0x03) } 181 | 182 | // SetWillQoS sets the WillQoS into the connect header flags. 183 | func (f *ConnectHeaderFlags) SetWillQoS(qos QoS) { 184 | *f &^= 0x18 185 | switch qos { 186 | case 1: 187 | *f |= 0x08 188 | case 2: 189 | *f |= 0x10 190 | } 191 | } 192 | 193 | var ( 194 | protocolMQIsdp = []byte("MQIsdp") 195 | protocolMQTT = []byte("MQTT") 196 | ) 197 | 198 | var ( 199 | errUnknownProtocolName = NewReasonCodeError(ProtocolError, "mqtt: unknown protocol name") 200 | errUnsupportedProtocolVersion = NewReasonCodeError(UnsupportedProtocolVersion, "mqtt: unsupported protocol version") 201 | ) 202 | 203 | func (r *PacketReader) readConnectHeader() { 204 | packet := r.packet.(*ConnectPacket) 205 | packet.ConnectHeader.ProtocolName, r.err = r.readBytes() 206 | if r.err != nil { 207 | return 208 | } 209 | switch { 210 | case bytes.Equal(packet.ConnectHeader.ProtocolName, protocolMQIsdp): 211 | packet.ConnectHeader.ProtocolName = protocolMQIsdp 212 | case bytes.Equal(packet.ConnectHeader.ProtocolName, protocolMQTT): 213 | packet.ConnectHeader.ProtocolName = protocolMQTT 214 | default: 215 | r.err = errUnknownProtocolName 216 | return 217 | } 218 | packet.ConnectHeader.ProtocolVersion, r.err = r.readByte() 219 | if r.err != nil { 220 | return 221 | } 222 | switch packet.ConnectHeader.ProtocolVersion { 223 | case 3, 4, 5: 224 | default: 225 | r.err = errUnsupportedProtocolVersion 226 | return 227 | } 228 | var f byte 229 | f, r.err = r.readByte() 230 | if r.err != nil { 231 | return 232 | } 233 | packet.ConnectHeader.ConnectHeaderFlags = ConnectHeaderFlags(f) 234 | if r.err = r.validateConnectHeaderFlags(packet.ConnectHeader.ConnectHeaderFlags); r.err != nil { 235 | return 236 | } 237 | packet.ConnectHeader.KeepAlive, r.err = r.readUint16() 238 | if r.err != nil { 239 | return 240 | } 241 | } 242 | 243 | func (w *PacketWriter) writeConnectHeader() { 244 | packet := w.packet.(*ConnectPacket) 245 | protocolName := packet.ConnectHeader.ProtocolName 246 | if len(protocolName) == 0 { 247 | switch w.protocol { 248 | case 3: 249 | protocolName = protocolMQIsdp 250 | case 4, 5: 251 | protocolName = protocolMQTT 252 | default: 253 | w.err = errUnsupportedProtocolVersion 254 | return 255 | } 256 | } 257 | w.err = w.writeBytes(protocolName) 258 | if w.err != nil { 259 | return 260 | } 261 | protocolVersion := packet.ConnectHeader.ProtocolVersion 262 | if protocolVersion == 0 { 263 | protocolVersion = w.protocol 264 | } 265 | w.err = w.writeByte(protocolVersion) 266 | if w.err != nil { 267 | return 268 | } 269 | w.err = w.writeByte(byte(packet.ConnectHeader.ConnectHeaderFlags)) 270 | if w.err != nil { 271 | return 272 | } 273 | w.err = w.writeUint16(packet.ConnectHeader.KeepAlive) 274 | if w.err != nil { 275 | return 276 | } 277 | } 278 | 279 | // ConnectPayload is the payload of the Connect packet. 280 | type ConnectPayload struct { 281 | ClientIdentifier []byte 282 | WillProperties Properties 283 | WillTopic []byte 284 | WillMessage []byte 285 | Username []byte 286 | Password []byte 287 | } 288 | 289 | var errEmptyClientIdentifier = NewReasonCodeError(ClientIdentifierNotValid, "mqtt: empty client identifier") 290 | 291 | func (r *PacketReader) readConnectPayload() { 292 | packet := r.packet.(*ConnectPacket) 293 | packet.ConnectPayload.ClientIdentifier, r.err = r.readBytes() 294 | if r.err != nil { 295 | return 296 | } 297 | if r.protocol < 5 && len(packet.ConnectPayload.ClientIdentifier) == 0 && !packet.ConnectHeader.CleanSession() { 298 | r.err = errEmptyClientIdentifier 299 | return 300 | } 301 | if packet.ConnectHeader.Will() { 302 | if r.protocol >= 5 { 303 | packet.ConnectPayload.WillProperties = r.readProperties() 304 | if r.err != nil { 305 | return 306 | } 307 | r.err = r.validateProperties(packet.ConnectPayload.WillProperties, willProperties) 308 | if r.err != nil { 309 | return 310 | } 311 | } 312 | packet.ConnectPayload.WillTopic, r.err = r.readBytes() 313 | if r.err != nil { 314 | return 315 | } 316 | packet.ConnectPayload.WillMessage, r.err = r.readBytes() 317 | if r.err != nil { 318 | return 319 | } 320 | } 321 | if packet.ConnectHeader.Username() { 322 | packet.ConnectPayload.Username, r.err = r.readBytes() 323 | if r.err != nil { 324 | return 325 | } 326 | } 327 | if packet.ConnectHeader.Password() { 328 | packet.ConnectPayload.Password, r.err = r.readBytes() 329 | if r.err != nil { 330 | return 331 | } 332 | } 333 | } 334 | 335 | func (w *PacketWriter) writeConnectPayload() { 336 | packet := w.packet.(*ConnectPacket) 337 | w.err = w.writeBytes(packet.ConnectPayload.ClientIdentifier) 338 | if w.err != nil { 339 | return 340 | } 341 | if w.protocol < 5 && len(packet.ConnectPayload.ClientIdentifier) == 0 && !packet.ConnectHeader.CleanSession() { 342 | w.err = errEmptyClientIdentifier 343 | return 344 | } 345 | if packet.ConnectHeader.Will() { 346 | if w.protocol >= 5 { 347 | w.writeProperties(packet.ConnectPayload.WillProperties) 348 | if w.err != nil { 349 | return 350 | } 351 | } 352 | w.err = w.writeBytes(packet.ConnectPayload.WillTopic) 353 | if w.err != nil { 354 | return 355 | } 356 | w.err = w.writeBytes(packet.ConnectPayload.WillMessage) 357 | if w.err != nil { 358 | return 359 | } 360 | } 361 | if packet.ConnectHeader.Username() { 362 | w.err = w.writeBytes(packet.ConnectPayload.Username) 363 | if w.err != nil { 364 | return 365 | } 366 | } 367 | if packet.ConnectHeader.Password() { 368 | w.err = w.writeBytes(packet.ConnectPayload.Password) 369 | if w.err != nil { 370 | return 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /connect_test.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateConnectHeaderFlags(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | r := PacketReader{protocol: 4} 13 | 14 | tests := []struct { 15 | flags ConnectHeaderFlags 16 | valid bool 17 | }{ 18 | {0x01, false}, 19 | {0x02, true}, 20 | {0x18, false}, 21 | } 22 | 23 | for _, test := range tests { 24 | err := r.validateConnectHeaderFlags(test.flags) 25 | if test.valid { 26 | assert.NoError(err) 27 | } else { 28 | assert.Error(err) 29 | } 30 | } 31 | } 32 | 33 | func TestConnectHeaderFlags(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | var f ConnectHeaderFlags 37 | 38 | assert.False(f.Username()) 39 | assert.False(f.Password()) 40 | assert.False(f.WillRetain()) 41 | assert.Equal(QoS0, f.WillQoS()) 42 | assert.False(f.Will()) 43 | assert.False(f.CleanSession()) 44 | 45 | f.SetUsername(true) 46 | f.SetPassword(true) 47 | f.SetWillRetain(true) 48 | f.SetWillQoS(QoS1) 49 | f.SetWill(true) 50 | f.SetCleanSession(true) 51 | 52 | assert.True(f.Username()) 53 | assert.True(f.Password()) 54 | assert.True(f.WillRetain()) 55 | assert.Equal(QoS1, f.WillQoS()) 56 | assert.True(f.Will()) 57 | assert.True(f.CleanSession()) 58 | 59 | f.SetUsername(false) 60 | f.SetPassword(false) 61 | f.SetWillRetain(false) 62 | f.SetWillQoS(QoS2) 63 | f.SetWill(false) 64 | f.SetCleanSession(false) 65 | 66 | assert.False(f.Username()) 67 | assert.False(f.Password()) 68 | assert.False(f.WillRetain()) 69 | assert.Equal(QoS2, f.WillQoS()) 70 | assert.False(f.Will()) 71 | assert.False(f.CleanSession()) 72 | } 73 | 74 | func TestConnectPacket(t *testing.T) { 75 | assert := assert.New(t) 76 | 77 | var p ConnectPacket 78 | 79 | assert.Nil(p.Username()) 80 | assert.Nil(p.Password()) 81 | properties, topic, message := p.Will() 82 | assert.Nil(properties) 83 | assert.Nil(topic) 84 | assert.Nil(message) 85 | 86 | p.SetUsername([]byte("username")) 87 | p.SetPassword([]byte("password")) 88 | p.SetWill(nil, []byte("will-topic"), []byte("will-message")) 89 | 90 | assert.True(p.ConnectHeader.Username()) 91 | assert.True(p.ConnectHeader.Password()) 92 | assert.True(p.ConnectHeader.Will()) 93 | 94 | assert.Equal([]byte("username"), p.Username()) 95 | assert.Equal([]byte("password"), p.Password()) 96 | _, topic, message = p.Will() 97 | assert.Equal([]byte("will-topic"), topic) 98 | assert.Equal([]byte("will-message"), message) 99 | } 100 | -------------------------------------------------------------------------------- /disconnect.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // DisconnectPacket is the Disconnect packet. 4 | type DisconnectPacket struct { 5 | DisconnectHeader 6 | Properties 7 | } 8 | 9 | func (*DisconnectPacket) _isPacket() {} 10 | 11 | // PacketType returns the packet type of the Disconnect packet. 12 | func (DisconnectPacket) PacketType() PacketType { return DISCONNECT } 13 | 14 | func (p DisconnectPacket) fixedHeader(protocol byte) (h FixedHeader) { 15 | h.SetPacketType(DISCONNECT) 16 | h.remainingLength = p.size(protocol) 17 | return 18 | } 19 | 20 | func (p DisconnectPacket) size(protocol byte) uint32 { 21 | size := 0 22 | if protocol >= 5 { 23 | size++ 24 | size += int(p.Properties.size()) 25 | } 26 | return uint32(size) 27 | } 28 | 29 | // DisconnectHeader is the header of the Disconnect packet. 30 | type DisconnectHeader struct { 31 | ReasonCode 32 | } 33 | 34 | func (r *PacketReader) readDisconnectHeader() { 35 | packet := r.packet.(*DisconnectPacket) 36 | if r.protocol >= 5 && r.remaining() > 0 { 37 | var f byte 38 | if f, r.err = r.readByte(); r.err != nil { 39 | return 40 | } 41 | packet.DisconnectHeader.ReasonCode = ReasonCode(f) 42 | } 43 | } 44 | 45 | func (w *PacketWriter) writeDisconnectHeader() { 46 | packet := w.packet.(*DisconnectPacket) 47 | if w.protocol >= 5 { 48 | w.err = w.writeByte(byte(packet.DisconnectHeader.ReasonCode)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example_client_test.go: -------------------------------------------------------------------------------- 1 | package mqtt_test 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "time" 7 | 8 | "htdvisser.dev/mqtt" 9 | ) 10 | 11 | func Example_client() { 12 | conn, err := net.Dial("tcp", "localhost:1883") 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | defer conn.Close() 17 | 18 | reader, writer := mqtt.NewReader(conn), mqtt.NewWriter(conn) 19 | 20 | connect := new(mqtt.ConnectPacket) 21 | connect.SetCleanSession(true) 22 | connect.SetUsername([]byte("username")) 23 | connect.SetPassword([]byte("password")) 24 | 25 | if err = writer.WritePacket(connect); err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | packet, err := reader.ReadPacket() 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | if packet.PacketType() != mqtt.CONNACK { 34 | log.Fatal("first packet was not CONNACK") 35 | } 36 | connack := packet.(*mqtt.ConnackPacket) 37 | 38 | if connack.ReasonCode != mqtt.Success { 39 | // TODO: Check actual reason code 40 | log.Fatal("connect failed") 41 | } 42 | 43 | go func() { 44 | subscribe := new(mqtt.SubscribePacket) 45 | subscribe.PacketIdentifier = 1 // TODO: Keep track of these. 46 | subscribe.SubscribePayload = append(subscribe.SubscribePayload, mqtt.Subscription{ 47 | TopicFilter: []byte("time/+"), 48 | QoS: 1, 49 | }) 50 | if err := writer.WritePacket(subscribe); err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | for t := range time.Tick(time.Second) { 55 | publish := new(mqtt.PublishPacket) 56 | publish.SetRetain(true) 57 | publish.TopicName = []byte("time/now") 58 | publish.PublishPayload = []byte(t.Format(time.RFC3339)) 59 | if err = writer.WritePacket(publish); err != nil { 60 | log.Fatal(err) 61 | } 62 | } 63 | }() 64 | 65 | for { 66 | packet, err := reader.ReadPacket() 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | switch packet.PacketType() { 72 | case mqtt.PUBLISH: 73 | publish := packet.(*mqtt.PublishPacket) 74 | switch publish.QoS() { 75 | case mqtt.QoS1: 76 | puback := publish.Puback() 77 | // TODO: Handle QoS 1 publish 78 | err = writer.WritePacket(puback) 79 | case mqtt.QoS2: 80 | pubrec := publish.Pubrec() 81 | // TODO: Handle QoS 2 publish 82 | err = writer.WritePacket(pubrec) 83 | } 84 | case mqtt.PUBACK: 85 | puback := packet.(*mqtt.PubackPacket) 86 | // TODO: Handle QoS 1 publish 87 | _ = puback.PacketIdentifier 88 | case mqtt.PUBREC: 89 | pubrec := packet.(*mqtt.PubrecPacket) 90 | pubrel := pubrec.Pubrel() 91 | // TODO: Handle QoS 2 publish 92 | err = writer.WritePacket(pubrel) 93 | case mqtt.PUBREL: 94 | pubrel := packet.(*mqtt.PubrelPacket) 95 | pubcomp := pubrel.Pubcomp() 96 | // TODO: Handle QoS 2 publish 97 | err = writer.WritePacket(pubcomp) 98 | case mqtt.PUBCOMP: 99 | pubcomp := packet.(*mqtt.PubcompPacket) 100 | // TODO: Handle QoS 2 publish 101 | _ = pubcomp.PacketIdentifier 102 | case mqtt.SUBACK: 103 | suback := packet.(*mqtt.SubackPacket) 104 | // TODO: Find subscribe by ID 105 | _ = suback.PacketIdentifier 106 | // TODO: Handle reason codes for subscribes 107 | _ = suback.SubackPayload 108 | case mqtt.UNSUBACK: 109 | unsuback := packet.(*mqtt.UnsubackPacket) 110 | // TODO: Find unsubscribe by ID 111 | _ = unsuback.PacketIdentifier 112 | // TODO: Handle reason codes for unsubscribes 113 | _ = unsuback.UnsubackPayload 114 | case mqtt.PINGRESP: 115 | // TODO: Handle pingresp 116 | case mqtt.DISCONNECT: 117 | disconnect := packet.(*mqtt.DisconnectPacket) 118 | _ = disconnect.ReasonCode 119 | // TODO: Handle disconnect 120 | return 121 | case mqtt.AUTH: 122 | auth := packet.(*mqtt.AuthPacket) 123 | if auth.ReasonCode != mqtt.ContinueAuthentication { 124 | log.Fatal("received auth packet with invalid reason code") 125 | } 126 | // TODO: Handle auth 127 | } 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /example_server_test.go: -------------------------------------------------------------------------------- 1 | package mqtt_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "htdvisser.dev/mqtt" 12 | ) 13 | 14 | func ListenAndAccept(address string, handle func(net.Conn) error) error { 15 | lis, err := net.Listen("tcp", address) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | for { 20 | conn, err := lis.Accept() 21 | if err != nil { 22 | return err 23 | } 24 | go func() { 25 | defer conn.Close() 26 | handle(conn) 27 | }() 28 | } 29 | } 30 | 31 | func checkAuth(username, password []byte) error { 32 | return nil 33 | } 34 | 35 | func Example_server() { 36 | ListenAndAccept("localhost:1883", func(conn net.Conn) error { 37 | ctx, cancel := context.WithCancel(context.Background()) 38 | var wg sync.WaitGroup 39 | defer func() { 40 | cancel() 41 | wg.Wait() 42 | }() 43 | 44 | reader, writer := mqtt.NewReader(conn), mqtt.NewWriter(conn) 45 | 46 | timeout := 10 * time.Second 47 | 48 | conn.SetReadDeadline(time.Now().Add(timeout)) // Read deadline for CONNECT. 49 | 50 | packet, err := reader.ReadPacket() 51 | if err != nil { 52 | return err 53 | } 54 | if packet.PacketType() != mqtt.CONNECT { 55 | return errors.New("first packet was not CONNECT") 56 | } 57 | connect := packet.(*mqtt.ConnectPacket) 58 | writer.SetProtocol(connect.ProtocolVersion) 59 | 60 | conn.SetWriteDeadline(time.Now().Add(timeout)) // Write deadline for CONNACK. 61 | 62 | connack := connect.Connack() 63 | 64 | if err := checkAuth(connect.Username(), connect.Password()); err != nil { 65 | connack.ReasonCode = mqtt.BadUsernameOrPassword 66 | return writer.WritePacket(connack) 67 | } 68 | 69 | if connect.KeepAlive != 0 { 70 | timeout = time.Duration(connect.KeepAlive) * 1500 * time.Millisecond 71 | } 72 | 73 | // TODO: Handle session, will, ... 74 | 75 | if err = writer.WritePacket(connack); err != nil { 76 | return err 77 | } 78 | 79 | conn.SetReadDeadline(time.Now().Add(timeout)) 80 | conn.SetWriteDeadline(time.Time{}) // Clear write deadline. 81 | 82 | var ( 83 | controlPackets = make(chan mqtt.Packet) 84 | publishPackets = make(chan *mqtt.PublishPacket) 85 | ) 86 | 87 | wg.Add(1) 88 | go func() { // Write routine 89 | defer wg.Done() 90 | for { 91 | select { 92 | case <-ctx.Done(): 93 | return 94 | case pkt := <-controlPackets: 95 | conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) 96 | err = writer.WritePacket(pkt) 97 | case pkt := <-publishPackets: 98 | conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) 99 | err = writer.WritePacket(pkt) 100 | } 101 | if err != nil { 102 | // TODO: Handle error 103 | } 104 | } 105 | }() 106 | 107 | for { // Read routine 108 | conn.SetReadDeadline(time.Now().Add(timeout)) 109 | packet, err := reader.ReadPacket() 110 | if err != nil { 111 | return err 112 | } 113 | 114 | switch packet.PacketType() { 115 | case mqtt.PUBLISH: 116 | publish := packet.(*mqtt.PublishPacket) 117 | switch publish.QoS() { 118 | case mqtt.QoS1: 119 | puback := publish.Puback() 120 | // TODO: Handle QoS 1 publish 121 | controlPackets <- puback 122 | case mqtt.QoS2: 123 | pubrec := publish.Pubrec() 124 | // TODO: Handle QoS 2 publish 125 | controlPackets <- pubrec 126 | } 127 | case mqtt.PUBACK: 128 | puback := packet.(*mqtt.PubackPacket) 129 | // TODO: Handle QoS 1 publish 130 | _ = puback.PacketIdentifier 131 | case mqtt.PUBREC: 132 | pubrec := packet.(*mqtt.PubrecPacket) 133 | pubrel := pubrec.Pubrel() 134 | // TODO: Handle QoS 2 publish 135 | controlPackets <- pubrel 136 | case mqtt.PUBREL: 137 | pubrel := packet.(*mqtt.PubrelPacket) 138 | pubcomp := pubrel.Pubcomp() 139 | // TODO: Handle QoS 2 publish 140 | controlPackets <- pubcomp 141 | case mqtt.PUBCOMP: 142 | pubcomp := packet.(*mqtt.PubcompPacket) 143 | // TODO: Handle QoS 2 publish 144 | _ = pubcomp.PacketIdentifier 145 | case mqtt.SUBSCRIBE: 146 | subscribe := packet.(*mqtt.SubscribePacket) 147 | suback := subscribe.Suback() 148 | // TODO: Handle subscribe, set reason codes in suback 149 | controlPackets <- suback 150 | case mqtt.UNSUBSCRIBE: 151 | unsubscribe := packet.(*mqtt.UnsubscribePacket) 152 | unsuback := unsubscribe.Unsuback() 153 | // TODO: Handle unsubscribe, set reason codes in unsuback 154 | controlPackets <- unsuback 155 | case mqtt.PINGREQ: 156 | pingreq := packet.(*mqtt.PingreqPacket) 157 | pingresp := pingreq.Pingresp() 158 | controlPackets <- pingresp 159 | case mqtt.DISCONNECT: 160 | disconnect := packet.(*mqtt.DisconnectPacket) 161 | _ = disconnect.ReasonCode 162 | // TODO: Handle disconnect 163 | return nil 164 | case mqtt.AUTH: 165 | auth := packet.(*mqtt.AuthPacket) 166 | if auth.ReasonCode != mqtt.ContinueAuthentication { 167 | return errors.New("received auth packet with invalid reason code") 168 | } 169 | // TODO: Handle auth 170 | } 171 | if err != nil { 172 | return err 173 | } 174 | } 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module htdvisser.dev/mqtt 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/stretchr/testify v1.4.0 8 | gopkg.in/yaml.v2 v2.2.7 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 10 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 14 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 15 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 16 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 17 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | // PacketType is the MQTT packet type. 9 | type PacketType byte 10 | 11 | // PacketType values. 12 | const ( 13 | _ PacketType = 0 // Reserved 14 | CONNECT PacketType = 1 // Client request to connect to Server 15 | CONNACK PacketType = 2 // Connect acknowledgment 16 | PUBLISH PacketType = 3 // Publish message 17 | PUBACK PacketType = 4 // Publish acknowledgment 18 | PUBREC PacketType = 5 // Publish received (assured delivery part 1) 19 | PUBREL PacketType = 6 // Publish release (assured delivery part 2) 20 | PUBCOMP PacketType = 7 // Publish complete (assured delivery part 3) 21 | SUBSCRIBE PacketType = 8 // Subscribe request 22 | SUBACK PacketType = 9 // Subscribe acknowledgment 23 | UNSUBSCRIBE PacketType = 10 // Unsubscribe request 24 | UNSUBACK PacketType = 11 // Unsubscribe acknowledgment 25 | PINGREQ PacketType = 12 // PING request 26 | PINGRESP PacketType = 13 // PING response 27 | DISCONNECT PacketType = 14 // Client is disconnecting 28 | AUTH PacketType = 15 // Authentication exchange 29 | ) 30 | 31 | func (t PacketType) String() string { 32 | switch t { 33 | case CONNECT: 34 | return "CONNECT" 35 | case CONNACK: 36 | return "CONNACK" 37 | case PUBLISH: 38 | return "PUBLISH" 39 | case PUBACK: 40 | return "PUBACK" 41 | case PUBREC: 42 | return "PUBREC" 43 | case PUBREL: 44 | return "PUBREL" 45 | case PUBCOMP: 46 | return "PUBCOMP" 47 | case SUBSCRIBE: 48 | return "SUBSCRIBE" 49 | case SUBACK: 50 | return "SUBACK" 51 | case UNSUBSCRIBE: 52 | return "UNSUBSCRIBE" 53 | case UNSUBACK: 54 | return "UNSUBACK" 55 | case PINGREQ: 56 | return "PINGREQ" 57 | case PINGRESP: 58 | return "PINGRESP" 59 | case DISCONNECT: 60 | return "DISCONNECT" 61 | case AUTH: 62 | return "AUTH" 63 | default: 64 | panic(fmt.Errorf("mqtt: unknown packet type 0x%x", byte(t))) 65 | } 66 | } 67 | 68 | // FixedHeader is the fixed header of an MQTT packet. 69 | type FixedHeader struct { 70 | typeAndFlags byte 71 | remainingLength uint32 72 | } 73 | 74 | // PacketType returns the packet type from the fixed header. 75 | func (h FixedHeader) PacketType() PacketType { 76 | return PacketType(h.typeAndFlags >> 4) 77 | } 78 | 79 | // SetPacketType sets the packet type into the fixed header. 80 | func (h *FixedHeader) SetPacketType(p PacketType) { 81 | h.typeAndFlags |= byte(p) << 4 82 | } 83 | 84 | var ( 85 | errReservedPacketType = NewReasonCodeError(MalformedPacket, "mqtt: reserved packed type") 86 | errInvalidHeaderFlags = NewReasonCodeError(MalformedPacket, "mqtt: invalid header flags") 87 | ) 88 | 89 | func (r *PacketReader) validateFixedHeader(h FixedHeader) error { 90 | switch h.PacketType() { 91 | case 0: 92 | return errReservedPacketType 93 | case PUBLISH: 94 | return r.validatePublishFlags(PublishFlags(h.typeAndFlags)) 95 | case PUBREL, SUBSCRIBE, UNSUBSCRIBE: 96 | if h.typeAndFlags&0x0F == 0x02 { 97 | return nil 98 | } 99 | default: 100 | if h.typeAndFlags&0x0F == 0x00 { 101 | return nil 102 | } 103 | } 104 | return errInvalidHeaderFlags 105 | } 106 | 107 | const maxRemainingLength = 268435455 108 | 109 | var errInvalidRemainingLength = NewReasonCodeError(ProtocolError, "mqtt: invalid remaining length") 110 | 111 | var errPacketTooLarge = NewReasonCodeError(PacketTooLarge, "mqtt: packet too large") 112 | 113 | func (r *PacketReader) readFixedHeader() { 114 | r.header.remainingLength = 1 // Enough to read the packet type and flags. 115 | r.header.typeAndFlags, r.err = r.readByte() 116 | if r.err != nil { 117 | return 118 | } 119 | r.header.remainingLength = 5 // Enough to read the "remaining length" field. 120 | var remainingLength uint64 121 | remainingLength, r.err = r.readUvarint() 122 | if r.err != nil { 123 | return 124 | } 125 | if remainingLength > maxRemainingLength { 126 | r.err = errInvalidRemainingLength 127 | return 128 | } 129 | r.header.remainingLength = uint32(remainingLength) 130 | r.err = r.validateFixedHeader(r.header) 131 | if r.err != nil { 132 | return 133 | } 134 | if r.maxPacketLength > 0 && r.nRead+r.header.remainingLength > r.maxPacketLength { 135 | r.err = errPacketTooLarge 136 | return 137 | } 138 | } 139 | 140 | func (w *PacketWriter) writeFixedHeader() (err error) { 141 | header := w.packet.fixedHeader(w.protocol) 142 | if header.remainingLength > maxRemainingLength { 143 | return errInvalidRemainingLength 144 | } 145 | var buf [5]byte 146 | buf[0] = header.typeAndFlags 147 | n := binary.PutUvarint(buf[1:], uint64(header.remainingLength)) 148 | _, err = w.w.Write(buf[:n+1]) 149 | return err 150 | } 151 | -------------------------------------------------------------------------------- /header_test.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPacketType(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | tests := []struct { 14 | b byte 15 | want PacketType 16 | }{ 17 | {0x10, CONNECT}, 18 | {0x20, CONNACK}, 19 | {0x30, PUBLISH}, 20 | {0x40, PUBACK}, 21 | {0x50, PUBREC}, 22 | {0x60, PUBREL}, 23 | {0x70, PUBCOMP}, 24 | {0x80, SUBSCRIBE}, 25 | {0x90, SUBACK}, 26 | {0xa0, UNSUBSCRIBE}, 27 | {0xb0, UNSUBACK}, 28 | {0xc0, PINGREQ}, 29 | {0xd0, PINGRESP}, 30 | {0xe0, DISCONNECT}, 31 | {0xf0, AUTH}, 32 | } 33 | 34 | for _, test := range tests { 35 | assert.Equal(test.want, (FixedHeader{typeAndFlags: test.b}).PacketType()) 36 | } 37 | } 38 | 39 | type headerTestPacket struct { 40 | FixedHeader 41 | } 42 | 43 | func (p headerTestPacket) _isPacket() {} 44 | func (p headerTestPacket) PacketType() PacketType { return p.FixedHeader.PacketType() } 45 | func (p headerTestPacket) fixedHeader(_ byte) FixedHeader { return p.FixedHeader } 46 | 47 | func TestReadWriteFixedHeader(t *testing.T) { 48 | assert := assert.New(t) 49 | 50 | tests := []struct { 51 | bin []byte 52 | header FixedHeader 53 | }{ 54 | {[]byte{0x30, 0x00}, FixedHeader{typeAndFlags: 0x30, remainingLength: 0}}, 55 | {[]byte{0x30, 0x7F}, FixedHeader{typeAndFlags: 0x30, remainingLength: 127}}, 56 | {[]byte{0x30, 0x80, 0x01}, FixedHeader{typeAndFlags: 0x30, remainingLength: 128}}, 57 | {[]byte{0x30, 0xFF, 0x7F}, FixedHeader{typeAndFlags: 0x30, remainingLength: 16383}}, 58 | {[]byte{0x30, 0x80, 0x80, 0x01}, FixedHeader{typeAndFlags: 0x30, remainingLength: 16384}}, 59 | {[]byte{0x30, 0xFF, 0xFF, 0x7F}, FixedHeader{typeAndFlags: 0x30, remainingLength: 2097151}}, 60 | {[]byte{0x30, 0x80, 0x80, 0x80, 0x01}, FixedHeader{typeAndFlags: 0x30, remainingLength: 2097152}}, 61 | {[]byte{0x30, 0xFF, 0xFF, 0xFF, 0x7F}, FixedHeader{typeAndFlags: 0x30, remainingLength: 268435455}}, 62 | } 63 | 64 | for _, test := range tests { 65 | r := NewReader(bytes.NewBuffer(test.bin)) 66 | r.readFixedHeader() 67 | assert.NoError(r.err) 68 | assert.Equal(test.header, r.header) 69 | 70 | buf := &bytes.Buffer{} 71 | w := NewWriter(buf) 72 | w.packet = headerTestPacket{test.header} 73 | w.writeFixedHeader() 74 | assert.NoError(r.err) 75 | assert.Equal(test.bin, buf.Bytes()) 76 | } 77 | } 78 | 79 | func TestValidateFixedHeader(t *testing.T) { 80 | assert := assert.New(t) 81 | 82 | r := PacketReader{protocol: 4} 83 | 84 | tests := []struct { 85 | flags byte 86 | valid bool 87 | }{ 88 | {0x10, true}, 89 | {0x11, false}, 90 | {0x3D, true}, 91 | {0x82, true}, 92 | {0x80, false}, 93 | } 94 | 95 | for _, test := range tests { 96 | err := r.validateFixedHeader(FixedHeader{typeAndFlags: test.flags}) 97 | if test.valid { 98 | assert.NoError(err) 99 | } else { 100 | assert.Error(err) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /mqtt.go: -------------------------------------------------------------------------------- 1 | // Package mqtt implements MQTT 3.1.1 and MQTT 5.0 packet types as well as a reader and a writer. 2 | package mqtt // import "htdvisser.dev/mqtt" 3 | 4 | // DefaultProtocolVersion is the default MQTT protocol version to use. 5 | var DefaultProtocolVersion byte = 4 6 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // Packet interface for MQTT packets from this package. 4 | type Packet interface { 5 | _isPacket() 6 | PacketType() PacketType 7 | fixedHeader(protocol byte) FixedHeader 8 | } 9 | -------------------------------------------------------------------------------- /pingreq.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // PingreqPacket is the Pingreq packet. 4 | type PingreqPacket struct{} 5 | 6 | func (*PingreqPacket) _isPacket() {} 7 | 8 | // PacketType returns the packet type of the Pingreq packet. 9 | func (PingreqPacket) PacketType() PacketType { return PINGREQ } 10 | 11 | func (p PingreqPacket) fixedHeader(_ byte) (h FixedHeader) { 12 | h.SetPacketType(PINGREQ) 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /pingresp.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // PingrespPacket is the Pingresp packet. 4 | type PingrespPacket struct{} 5 | 6 | func (*PingrespPacket) _isPacket() {} 7 | 8 | // PacketType returns the packet type of the Pingresp packet. 9 | func (PingrespPacket) PacketType() PacketType { return PINGRESP } 10 | 11 | func (p PingrespPacket) fixedHeader(_ byte) (h FixedHeader) { 12 | h.SetPacketType(PINGRESP) 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /properties.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | // Properties is a slice of MQTT Properties. 9 | type Properties []Property 10 | 11 | func (properties Properties) size() uint32 { 12 | size := properties.innerSize() 13 | size += uint32(binary.PutUvarint(make([]byte, 4), uint64(size))) 14 | return size 15 | } 16 | 17 | func (properties Properties) innerSize() (size uint32) { 18 | for _, property := range properties { 19 | size += property.size() 20 | } 21 | return size 22 | } 23 | 24 | // Property is a single MQTT Property 25 | type Property struct { 26 | Identifier PropertyIdentifier 27 | UintValue uint64 // used for all uints 28 | BytesValue []byte // used for bytes and strings 29 | StringPairValue StringPair 30 | ByteValue byte 31 | } 32 | 33 | func (p Property) size() uint32 { 34 | switch p.Identifier { 35 | // UintValue: 36 | case SubscriptionIdentifier: 37 | return 1 + uint32(binary.PutUvarint(make([]byte, 4), p.UintValue)) 38 | // Uint32Value: 39 | case MessageExpiryInterval, 40 | SessionExpiryInterval, 41 | WillDelayInterval, 42 | MaximumPacketSize: 43 | return 1 + 4 44 | // Uint16Value: 45 | case ServerKeepAlive, 46 | ReceiveMaximum, 47 | TopicAliasMaximum, 48 | TopicAlias: 49 | return 1 + 2 50 | // StringValue: 51 | case ContentType, 52 | ResponseTopic, 53 | AssignedClientIdentifier, 54 | AuthenticationMethod, 55 | ResponseInformation, 56 | ServerReference, 57 | ReasonString: 58 | return 1 + 2 + uint32(len(p.BytesValue)) 59 | // BytesValue: 60 | case CorrelationData, 61 | AuthenticationData: 62 | return 1 + 2 + uint32(len(p.BytesValue)) 63 | // StringPairValue: 64 | case UserProperty: 65 | return 1 + 2 + uint32(len(p.StringPairValue.Key)) + 2 + uint32(len(p.StringPairValue.Value)) 66 | // ByteValue: 67 | case PayloadFormatIndicator, 68 | RequestProblemInformation, 69 | RequestResponseInformation, 70 | MaximumQoS, 71 | RetainAvailable, 72 | WildcardSubscriptionAvailable, 73 | SubscriptionIdentifierAvailable, 74 | SharedSubscriptionAvailable: 75 | return 1 + 1 76 | default: 77 | panic(errUnknownProperty) 78 | } 79 | } 80 | 81 | // PropertyIdentifier is the identifier for MQTT properties. 82 | type PropertyIdentifier uint64 83 | 84 | // PropertyIdentifier values. 85 | const ( 86 | _ PropertyIdentifier = 0 // Reserved 87 | PayloadFormatIndicator = 1 // Payload Format Indicator 88 | MessageExpiryInterval = 2 // Message Expiry Interval 89 | ContentType = 3 // Content Type 90 | ResponseTopic = 8 // Response Topic 91 | CorrelationData = 9 // Correlation Data 92 | SubscriptionIdentifier = 11 // Subscription Identifier 93 | SessionExpiryInterval = 17 // Session Expiry Interval 94 | AssignedClientIdentifier = 18 // Assigned Client Identifier 95 | ServerKeepAlive = 19 // Server Keep Alive 96 | AuthenticationMethod = 21 // Authentication Method 97 | AuthenticationData = 22 // Authentication Data 98 | RequestProblemInformation = 23 // Request Problem Information 99 | WillDelayInterval = 24 // Will Delay Interval 100 | RequestResponseInformation = 25 // Request Response Information 101 | ResponseInformation = 26 // Response Information 102 | ServerReference = 28 // Server Reference 103 | ReasonString = 31 // Reason String 104 | ReceiveMaximum = 33 // Receive Maximum 105 | TopicAliasMaximum = 34 // Topic Alias Maximum 106 | TopicAlias = 35 // Topic Alias 107 | MaximumQoS = 36 // Maximum QoS 108 | RetainAvailable = 37 // Retain Available 109 | UserProperty = 38 // User Property 110 | MaximumPacketSize = 39 // Maximum Packet Size 111 | WildcardSubscriptionAvailable = 40 // Wildcard Subscription Available 112 | SubscriptionIdentifierAvailable = 41 // Subscription Identifier Available 113 | SharedSubscriptionAvailable = 42 // Shared Subscription Available 114 | ) 115 | 116 | const willProperties PacketType = 255 117 | 118 | var allowedPropertyIdentifiers = make(map[PropertyIdentifier]map[PacketType]bool) 119 | 120 | func allowPropertyIdentifier(id PropertyIdentifier, packetTypes ...PacketType) { 121 | allowed, ok := allowedPropertyIdentifiers[id] 122 | if !ok { 123 | allowed = make(map[PacketType]bool) 124 | allowedPropertyIdentifiers[id] = allowed 125 | } 126 | for _, packetType := range packetTypes { 127 | allowed[packetType] = true 128 | } 129 | } 130 | 131 | func init() { 132 | allowPropertyIdentifier(PayloadFormatIndicator, PUBLISH, willProperties) 133 | allowPropertyIdentifier(MessageExpiryInterval, PUBLISH, willProperties) 134 | allowPropertyIdentifier(ContentType, PUBLISH, willProperties) 135 | allowPropertyIdentifier(ResponseTopic, PUBLISH, willProperties) 136 | allowPropertyIdentifier(CorrelationData, PUBLISH, willProperties) 137 | allowPropertyIdentifier(SubscriptionIdentifier, PUBLISH, SUBSCRIBE) 138 | allowPropertyIdentifier(SessionExpiryInterval, CONNECT, CONNACK, DISCONNECT) 139 | allowPropertyIdentifier(AssignedClientIdentifier, CONNACK) 140 | allowPropertyIdentifier(ServerKeepAlive, CONNACK) 141 | allowPropertyIdentifier(AuthenticationMethod, CONNECT, CONNACK, AUTH) 142 | allowPropertyIdentifier(AuthenticationData, CONNECT, CONNACK, AUTH) 143 | allowPropertyIdentifier(RequestProblemInformation, CONNECT) 144 | allowPropertyIdentifier(WillDelayInterval, willProperties) 145 | allowPropertyIdentifier(RequestResponseInformation, CONNECT) 146 | allowPropertyIdentifier(ResponseInformation, CONNACK) 147 | allowPropertyIdentifier(ServerReference, CONNACK, DISCONNECT) 148 | allowPropertyIdentifier(ReasonString, CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, DISCONNECT, AUTH) 149 | allowPropertyIdentifier(ReceiveMaximum, CONNECT, CONNACK) 150 | allowPropertyIdentifier(TopicAliasMaximum, CONNECT, CONNACK) 151 | allowPropertyIdentifier(TopicAlias, PUBLISH) 152 | allowPropertyIdentifier(MaximumQoS, CONNACK) 153 | allowPropertyIdentifier(RetainAvailable, CONNACK) 154 | allowPropertyIdentifier(UserProperty, CONNECT, CONNACK, PUBLISH, willProperties, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, AUTH) 155 | allowPropertyIdentifier(MaximumPacketSize, CONNECT, CONNACK) 156 | allowPropertyIdentifier(WildcardSubscriptionAvailable, CONNACK) 157 | allowPropertyIdentifier(SubscriptionIdentifierAvailable, CONNACK) 158 | allowPropertyIdentifier(SharedSubscriptionAvailable, CONNACK) 159 | } 160 | 161 | func (r *PacketReader) validateProperties(properties Properties, packetType PacketType) error { 162 | for _, property := range properties { 163 | if !allowedPropertyIdentifiers[property.Identifier][packetType] { 164 | return NewReasonCodeError(MalformedPacket, fmt.Sprintf("mqtt: invalid property %d for %s", property.Identifier, packetType)) 165 | } 166 | } 167 | return nil 168 | } 169 | 170 | // StringPair is a key-value pair of MQTT strings. 171 | type StringPair struct { 172 | Key []byte 173 | Value []byte 174 | } 175 | 176 | // Strings returns the key-value pair as strings. 177 | func (p StringPair) Strings() (key, value string) { 178 | return string(p.Key), string(p.Value) 179 | } 180 | 181 | func (r *PacketReader) readPacketProperties() { 182 | if r.protocol < 5 || r.remaining() == 0 { 183 | return 184 | } 185 | var properties Properties 186 | switch pkt := r.packet.(type) { 187 | case *ConnectPacket: 188 | properties = r.readProperties() 189 | pkt.Properties = properties 190 | case *ConnackPacket: 191 | properties = r.readProperties() 192 | pkt.Properties = properties 193 | case *PublishPacket: 194 | properties = r.readProperties() 195 | pkt.Properties = properties 196 | case *PubackPacket: 197 | properties = r.readProperties() 198 | pkt.Properties = properties 199 | case *PubrecPacket: 200 | properties = r.readProperties() 201 | pkt.Properties = properties 202 | case *PubrelPacket: 203 | properties = r.readProperties() 204 | pkt.Properties = properties 205 | case *PubcompPacket: 206 | properties = r.readProperties() 207 | pkt.Properties = properties 208 | case *SubscribePacket: 209 | properties = r.readProperties() 210 | pkt.Properties = properties 211 | case *SubackPacket: 212 | properties = r.readProperties() 213 | pkt.Properties = properties 214 | case *UnsubscribePacket: 215 | properties = r.readProperties() 216 | pkt.Properties = properties 217 | case *UnsubackPacket: 218 | properties = r.readProperties() 219 | pkt.Properties = properties 220 | case *DisconnectPacket: 221 | properties = r.readProperties() 222 | pkt.Properties = properties 223 | case *AuthPacket: 224 | properties = r.readProperties() 225 | pkt.Properties = properties 226 | default: 227 | return 228 | } 229 | if r.err != nil { 230 | return 231 | } 232 | r.err = r.validateProperties(properties, r.packet.PacketType()) 233 | } 234 | 235 | func (r *PacketReader) readProperties() Properties { 236 | var properties Properties 237 | var propertyLength uint64 238 | if propertyLength, r.err = r.readUvarint(); r.err != nil { 239 | return nil 240 | } 241 | nReadBefore := r.nRead 242 | for uint64(r.nRead-nReadBefore) < propertyLength { 243 | property := r.readProperty() 244 | if r.err != nil { 245 | return nil 246 | } 247 | properties = append(properties, property) 248 | } 249 | return properties 250 | } 251 | 252 | var errUnknownProperty = NewReasonCodeError(ProtocolError, "mqtt: unknown property") 253 | 254 | func (r *PacketReader) readProperty() (p Property) { 255 | var id uint64 256 | if id, r.err = r.readUvarint(); r.err != nil { 257 | return 258 | } 259 | p.Identifier = PropertyIdentifier(id) 260 | switch p.Identifier { 261 | case PayloadFormatIndicator, 262 | RequestProblemInformation, 263 | RequestResponseInformation, 264 | MaximumQoS, 265 | RetainAvailable, 266 | WildcardSubscriptionAvailable, 267 | SubscriptionIdentifierAvailable, 268 | SharedSubscriptionAvailable: 269 | p.ByteValue, r.err = r.readByte() 270 | case MessageExpiryInterval, 271 | SessionExpiryInterval, 272 | WillDelayInterval, 273 | MaximumPacketSize: 274 | var v uint32 275 | v, r.err = r.readUint32() 276 | p.UintValue = uint64(v) 277 | case ContentType, 278 | ResponseTopic, 279 | AssignedClientIdentifier, 280 | AuthenticationMethod, 281 | ResponseInformation, 282 | ServerReference, 283 | ReasonString: 284 | p.BytesValue, r.err = r.readString() 285 | case CorrelationData, 286 | AuthenticationData: 287 | p.BytesValue, r.err = r.readBytes() 288 | case SubscriptionIdentifier: 289 | p.UintValue, r.err = r.readUvarint() 290 | case ServerKeepAlive, 291 | ReceiveMaximum, 292 | TopicAliasMaximum, 293 | TopicAlias: 294 | var v uint16 295 | v, r.err = r.readUint16() 296 | p.UintValue = uint64(v) 297 | case UserProperty: 298 | pair := StringPair{} 299 | pair.Key, pair.Value, r.err = r.readStringPair() 300 | p.StringPairValue = pair 301 | default: 302 | r.err = errUnknownProperty 303 | } 304 | return 305 | } 306 | 307 | func (w *PacketWriter) writePacketProperties() { 308 | if w.protocol < 5 { 309 | return 310 | } 311 | switch pkt := w.packet.(type) { 312 | case *ConnectPacket: 313 | w.writeProperties(pkt.Properties) 314 | case *ConnackPacket: 315 | w.writeProperties(pkt.Properties) 316 | case *PublishPacket: 317 | w.writeProperties(pkt.Properties) 318 | case *PubackPacket: 319 | w.writeProperties(pkt.Properties) 320 | case *PubrecPacket: 321 | w.writeProperties(pkt.Properties) 322 | case *PubrelPacket: 323 | w.writeProperties(pkt.Properties) 324 | case *PubcompPacket: 325 | w.writeProperties(pkt.Properties) 326 | case *SubscribePacket: 327 | w.writeProperties(pkt.Properties) 328 | case *SubackPacket: 329 | w.writeProperties(pkt.Properties) 330 | case *UnsubscribePacket: 331 | w.writeProperties(pkt.Properties) 332 | case *UnsubackPacket: 333 | w.writeProperties(pkt.Properties) 334 | case *DisconnectPacket: 335 | w.writeProperties(pkt.Properties) 336 | case *AuthPacket: 337 | w.writeProperties(pkt.Properties) 338 | default: 339 | return 340 | } 341 | } 342 | 343 | func (w *PacketWriter) writeProperties(p Properties) { 344 | if w.err = w.writeUvarint(p.innerSize()); w.err != nil { 345 | return 346 | } 347 | for _, property := range p { 348 | w.writeProperty(property) 349 | if w.err != nil { 350 | return 351 | } 352 | } 353 | } 354 | 355 | func (w *PacketWriter) writeProperty(p Property) { 356 | if w.err = w.writeUvarint(uint32(p.Identifier)); w.err != nil { 357 | return 358 | } 359 | switch p.Identifier { 360 | case PayloadFormatIndicator, 361 | RequestProblemInformation, 362 | RequestResponseInformation, 363 | MaximumQoS, 364 | RetainAvailable, 365 | WildcardSubscriptionAvailable, 366 | SubscriptionIdentifierAvailable, 367 | SharedSubscriptionAvailable: 368 | w.err = w.writeByte(p.ByteValue) 369 | case MessageExpiryInterval, 370 | SessionExpiryInterval, 371 | WillDelayInterval, 372 | MaximumPacketSize: 373 | w.err = w.writeUint32(uint32(p.UintValue)) 374 | case ContentType, 375 | ResponseTopic, 376 | AssignedClientIdentifier, 377 | AuthenticationMethod, 378 | ResponseInformation, 379 | ServerReference, 380 | ReasonString: 381 | w.err = w.writeBytes(p.BytesValue) 382 | case CorrelationData, 383 | AuthenticationData: 384 | w.err = w.writeBytes(p.BytesValue) 385 | case SubscriptionIdentifier: 386 | w.err = w.writeUvarint(uint32(p.UintValue)) 387 | case ServerKeepAlive, 388 | ReceiveMaximum, 389 | TopicAliasMaximum, 390 | TopicAlias: 391 | w.err = w.writeUint16(uint16(p.UintValue)) 392 | case UserProperty: 393 | if w.err = w.writeBytes(p.StringPairValue.Key); w.err != nil { 394 | return 395 | } 396 | w.err = w.writeBytes(p.StringPairValue.Value) 397 | default: 398 | w.err = errUnknownProperty 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /properties_test.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestReadWriteProperties(t *testing.T) { 12 | for id := range allowedPropertyIdentifiers { 13 | t.Run(fmt.Sprint(id), func(t *testing.T) { 14 | assert := assert.New(t) 15 | 16 | buf := &bytes.Buffer{} 17 | w := NewWriter(buf) 18 | 19 | subject := Property{Identifier: id} 20 | 21 | size := subject.size() 22 | 23 | w.writeProperty(subject) 24 | assert.NoError(w.err) 25 | 26 | assert.Equal(int(size), buf.Len()) 27 | 28 | buf = bytes.NewBuffer(buf.Bytes()) 29 | 30 | r := NewReader(buf) 31 | r.header.remainingLength = w.nWritten 32 | 33 | p := r.readProperty() 34 | assert.NoError(r.err) 35 | assert.Equal(subject, p) 36 | }) 37 | } 38 | 39 | var properties Properties 40 | for id := range allowedPropertyIdentifiers { 41 | properties = append(properties, Property{Identifier: id}) 42 | } 43 | 44 | assert := assert.New(t) 45 | 46 | buf := &bytes.Buffer{} 47 | w := NewWriter(buf) 48 | 49 | w.writeProperties(properties) 50 | assert.NoError(w.err) 51 | 52 | buf = bytes.NewBuffer(buf.Bytes()) 53 | 54 | r := NewReader(buf) 55 | r.header.remainingLength = w.nWritten 56 | 57 | p := r.readProperties() 58 | assert.NoError(r.err) 59 | assert.Equal(properties, p) 60 | } 61 | -------------------------------------------------------------------------------- /puback.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // PubackPacket is the Puback packet. 4 | type PubackPacket struct { 5 | PubackHeader 6 | Properties 7 | } 8 | 9 | func (*PubackPacket) _isPacket() {} 10 | 11 | // PacketType returns the packet type of the Puback packet. 12 | func (PubackPacket) PacketType() PacketType { return PUBACK } 13 | 14 | func (p PubackPacket) fixedHeader(protocol byte) (h FixedHeader) { 15 | h.SetPacketType(PUBACK) 16 | h.remainingLength = p.size(protocol) 17 | return 18 | } 19 | 20 | func (p PubackPacket) size(protocol byte) uint32 { 21 | size := 2 22 | if protocol >= 5 { 23 | size++ 24 | size += int(p.Properties.size()) 25 | } 26 | return uint32(size) 27 | } 28 | 29 | // PubackHeader is the header of the Puback packet. 30 | type PubackHeader struct { 31 | PacketIdentifier uint16 32 | ReasonCode 33 | } 34 | 35 | func (r *PacketReader) readPubackHeader() { 36 | packet := r.packet.(*PubackPacket) 37 | if packet.PubackHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 38 | return 39 | } 40 | if r.protocol >= 5 && r.remaining() > 0 { 41 | var f byte 42 | if f, r.err = r.readByte(); r.err != nil { 43 | return 44 | } 45 | packet.PubackHeader.ReasonCode = ReasonCode(f) 46 | } 47 | } 48 | 49 | func (w *PacketWriter) writePubackHeader() { 50 | packet := w.packet.(*PubackPacket) 51 | if w.err = w.writeUint16(packet.PubackHeader.PacketIdentifier); w.err != nil { 52 | return 53 | } 54 | if w.protocol >= 5 { 55 | w.err = w.writeByte(byte(packet.PubackHeader.ReasonCode)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pubcomp.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // PubcompPacket is the Pubcomp packet. 4 | type PubcompPacket struct { 5 | PubcompHeader 6 | Properties 7 | } 8 | 9 | func (*PubcompPacket) _isPacket() {} 10 | 11 | // PacketType returns the packet type of the Pubcomp packet. 12 | func (PubcompPacket) PacketType() PacketType { return PUBCOMP } 13 | 14 | func (p PubcompPacket) fixedHeader(protocol byte) (h FixedHeader) { 15 | h.SetPacketType(PUBCOMP) 16 | h.remainingLength = p.size(protocol) 17 | return 18 | } 19 | 20 | func (p PubcompPacket) size(protocol byte) uint32 { 21 | size := 2 22 | if protocol >= 5 { 23 | size++ 24 | size += int(p.Properties.size()) 25 | } 26 | return uint32(size) 27 | } 28 | 29 | // PubcompHeader is the header of the Pubcomp packet. 30 | type PubcompHeader struct { 31 | PacketIdentifier uint16 32 | ReasonCode 33 | } 34 | 35 | func (r *PacketReader) readPubcompHeader() { 36 | packet := r.packet.(*PubcompPacket) 37 | if packet.PubcompHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 38 | return 39 | } 40 | if r.protocol >= 5 && r.remaining() > 0 { 41 | var f byte 42 | if f, r.err = r.readByte(); r.err != nil { 43 | return 44 | } 45 | packet.PubcompHeader.ReasonCode = ReasonCode(f) 46 | } 47 | } 48 | 49 | func (w *PacketWriter) writePubcompHeader() { 50 | packet := w.packet.(*PubcompPacket) 51 | if w.err = w.writeUint16(packet.PubcompHeader.PacketIdentifier); w.err != nil { 52 | return 53 | } 54 | if w.protocol >= 5 { 55 | w.err = w.writeByte(byte(packet.PubcompHeader.ReasonCode)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /publish.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // PublishPacket is the Publish packet. 4 | type PublishPacket struct { 5 | PublishFlags 6 | PublishHeader 7 | Properties 8 | PublishPayload []byte 9 | } 10 | 11 | func (*PublishPacket) _isPacket() {} 12 | 13 | // PacketType returns the packet type of the Publish packet. 14 | func (PublishPacket) PacketType() PacketType { return PUBLISH } 15 | 16 | func (p PublishPacket) fixedHeader(protocol byte) (h FixedHeader) { 17 | h.SetPacketType(PUBLISH) 18 | h.typeAndFlags |= byte(p.PublishFlags) 19 | h.remainingLength = p.size(protocol) 20 | return 21 | } 22 | 23 | func (p PublishPacket) size(protocol byte) uint32 { 24 | size := 2 + len(p.PublishHeader.TopicName) 25 | if p.PublishFlags.QoS() > 0 { 26 | size += 2 27 | } 28 | if protocol >= 5 { 29 | size += int(p.Properties.size()) 30 | } 31 | size += len(p.PublishPayload) 32 | return uint32(size) 33 | } 34 | 35 | func (p PublishPacket) publishFlags() PublishFlags { return p.PublishFlags } 36 | 37 | // QoS is the MQTT quality of service of a Publish packet. 38 | type QoS byte 39 | 40 | // QoS values. 41 | const ( 42 | QoS0 QoS = 0 // At Most Once 43 | QoS1 QoS = 1 // At Least Once 44 | QoS2 QoS = 2 // Exactly Once 45 | ) 46 | 47 | func (r *PacketReader) validateQoS(qos QoS) error { 48 | if qos > 2 { 49 | return errInvalidQoS 50 | } 51 | return nil 52 | } 53 | 54 | // PublishFlags are the fixed header flags for a Publish packet. 55 | type PublishFlags byte 56 | 57 | var errInvalidQoS = NewReasonCodeError(MalformedPacket, "mqtt: invalid QoS") 58 | 59 | func (r *PacketReader) validatePublishFlags(f PublishFlags) error { 60 | return r.validateQoS(f.QoS()) 61 | } 62 | 63 | // Dup returns the Dup bit from the publish flags. 64 | func (f PublishFlags) Dup() bool { return f&0x8 == 0x8 } 65 | 66 | // SetDup sets the Dup bit into the publish flags. 67 | func (f *PublishFlags) SetDup(dup bool) { 68 | *f &^= 0x8 69 | if dup { 70 | *f |= 0x8 71 | } 72 | } 73 | 74 | // QoS returns the QoS from the publish flags. 75 | func (f PublishFlags) QoS() QoS { return QoS(f >> 1 & 0x03) } 76 | 77 | // SetQoS sets the QoS into the publish flags. 78 | func (f *PublishFlags) SetQoS(qos QoS) { 79 | *f &^= 0x6 80 | switch qos { 81 | case 1: 82 | *f |= 0x2 83 | case 2: 84 | *f |= 0x4 85 | } 86 | } 87 | 88 | // Retain returns the Retain bit from the publish flags. 89 | func (f PublishFlags) Retain() bool { return f&0x1 == 0x1 } 90 | 91 | // SetRetain sets the Retain bit into the publish flags. 92 | func (f *PublishFlags) SetRetain(retain bool) { 93 | *f &^= 0x1 94 | if retain { 95 | *f |= 0x1 96 | } 97 | } 98 | 99 | // PublishHeader is the header of the Publish packet. 100 | type PublishHeader struct { 101 | TopicName []byte 102 | PacketIdentifier uint16 103 | } 104 | 105 | func (r *PacketReader) readPublishHeader() { 106 | packet := r.packet.(*PublishPacket) 107 | if packet.PublishHeader.TopicName, r.err = r.readBytes(); r.err != nil { 108 | return 109 | } 110 | if packet.PublishFlags.QoS() > 0 { 111 | if packet.PublishHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 112 | return 113 | } 114 | } 115 | } 116 | 117 | func (w *PacketWriter) writePublishHeader() { 118 | packet := w.packet.(*PublishPacket) 119 | if w.err = w.writeBytes(packet.PublishHeader.TopicName); w.err != nil { 120 | return 121 | } 122 | if PublishFlags(packet.PublishFlags).QoS() > 0 { 123 | if w.err = w.writeUint16(packet.PublishHeader.PacketIdentifier); w.err != nil { 124 | return 125 | } 126 | } 127 | } 128 | 129 | func (r *PacketReader) readPublishPayload() { 130 | packet := r.packet.(*PublishPacket) 131 | packet.PublishPayload, r.err = r.readRemaining() 132 | } 133 | 134 | func (w *PacketWriter) writePublishPayload() { 135 | packet := w.packet.(*PublishPacket) 136 | w.err = w.write(packet.PublishPayload) 137 | } 138 | -------------------------------------------------------------------------------- /publish_test.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidatePublishFlags(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | r := PacketReader{protocol: 4} 13 | 14 | tests := []struct { 15 | flags PublishFlags 16 | valid bool 17 | }{ 18 | {0x0, true}, 19 | {0x1, true}, 20 | {0x2, true}, 21 | {0x3, true}, 22 | {0x4, true}, 23 | {0x5, true}, 24 | {0x6, false}, 25 | {0x7, false}, 26 | {0x8, true}, 27 | {0x9, true}, 28 | {0xa, true}, 29 | {0xb, true}, 30 | {0xc, true}, 31 | {0xd, true}, 32 | {0xe, false}, 33 | {0xf, false}, 34 | } 35 | 36 | for _, test := range tests { 37 | err := r.validatePublishFlags(test.flags) 38 | if test.valid { 39 | assert.NoError(err) 40 | } else { 41 | assert.Error(err) 42 | } 43 | } 44 | } 45 | 46 | func TestPublishFlags(t *testing.T) { 47 | assert := assert.New(t) 48 | var f PublishFlags 49 | 50 | assert.False(f.Dup()) 51 | assert.Equal(QoS0, f.QoS()) 52 | assert.False(f.Retain()) 53 | 54 | f.SetDup(true) 55 | f.SetQoS(QoS1) 56 | f.SetRetain(true) 57 | 58 | assert.True(f.Dup()) 59 | assert.Equal(QoS1, f.QoS()) 60 | assert.True(f.Retain()) 61 | 62 | f.SetDup(false) 63 | f.SetQoS(QoS2) 64 | f.SetRetain(false) 65 | 66 | assert.False(f.Dup()) 67 | assert.Equal(QoS2, f.QoS()) 68 | assert.False(f.Retain()) 69 | } 70 | -------------------------------------------------------------------------------- /pubrec.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // PubrecPacket is the Pubrec packet. 4 | type PubrecPacket struct { 5 | PubrecHeader 6 | Properties 7 | } 8 | 9 | func (*PubrecPacket) _isPacket() {} 10 | 11 | // PacketType returns the packet type of the Pubrec packet. 12 | func (PubrecPacket) PacketType() PacketType { return PUBREC } 13 | 14 | func (p PubrecPacket) fixedHeader(protocol byte) (h FixedHeader) { 15 | h.SetPacketType(PUBREC) 16 | h.remainingLength = p.size(protocol) 17 | return 18 | } 19 | 20 | func (p PubrecPacket) size(protocol byte) uint32 { 21 | size := 2 22 | if protocol >= 5 { 23 | size++ 24 | size += int(p.Properties.size()) 25 | } 26 | return uint32(size) 27 | } 28 | 29 | // PubrecHeader is the header of the Pubrec packet. 30 | type PubrecHeader struct { 31 | PacketIdentifier uint16 32 | ReasonCode 33 | } 34 | 35 | func (r *PacketReader) readPubrecHeader() { 36 | packet := r.packet.(*PubrecPacket) 37 | if packet.PubrecHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 38 | return 39 | } 40 | if r.protocol >= 5 && r.remaining() > 0 { 41 | var f byte 42 | if f, r.err = r.readByte(); r.err != nil { 43 | return 44 | } 45 | packet.PubrecHeader.ReasonCode = ReasonCode(f) 46 | } 47 | } 48 | 49 | func (w *PacketWriter) writePubrecHeader() { 50 | packet := w.packet.(*PubrecPacket) 51 | if w.err = w.writeUint16(packet.PubrecHeader.PacketIdentifier); w.err != nil { 52 | return 53 | } 54 | if w.protocol >= 5 { 55 | w.err = w.writeByte(byte(packet.PubrecHeader.ReasonCode)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pubrel.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // PubrelPacket is the Pubrel packet. 4 | type PubrelPacket struct { 5 | PubrelHeader 6 | Properties 7 | } 8 | 9 | func (*PubrelPacket) _isPacket() {} 10 | 11 | // PacketType returns the packet type of the Pubrel packet. 12 | func (PubrelPacket) PacketType() PacketType { return PUBREL } 13 | 14 | func (p PubrelPacket) fixedHeader(protocol byte) (h FixedHeader) { 15 | h.SetPacketType(PUBREL) 16 | h.typeAndFlags |= 0x02 17 | h.remainingLength = p.size(protocol) 18 | return 19 | } 20 | 21 | func (p PubrelPacket) size(protocol byte) uint32 { 22 | size := 2 23 | if protocol >= 5 { 24 | size++ 25 | size += int(p.Properties.size()) 26 | } 27 | return uint32(size) 28 | } 29 | 30 | // PubrelHeader is the header of the Pubrel packet. 31 | type PubrelHeader struct { 32 | PacketIdentifier uint16 33 | ReasonCode 34 | } 35 | 36 | func (r *PacketReader) readPubrelHeader() { 37 | packet := r.packet.(*PubrelPacket) 38 | if packet.PubrelHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 39 | return 40 | } 41 | if r.protocol >= 5 && r.remaining() > 0 { 42 | var f byte 43 | if f, r.err = r.readByte(); r.err != nil { 44 | return 45 | } 46 | packet.PubrelHeader.ReasonCode = ReasonCode(f) 47 | } 48 | } 49 | 50 | func (w *PacketWriter) writePubrelHeader() { 51 | packet := w.packet.(*PubrelPacket) 52 | if w.err = w.writeUint16(packet.PubrelHeader.PacketIdentifier); w.err != nil { 53 | return 54 | } 55 | if w.protocol >= 5 { 56 | w.err = w.writeByte(byte(packet.PubrelHeader.ReasonCode)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "io" 7 | "sync" 8 | "unicode/utf8" 9 | ) 10 | 11 | // ReaderOption is an option for the PacketReader. 12 | type ReaderOption interface { 13 | apply(*PacketReader) 14 | } 15 | 16 | type readerOptionFunc func(*PacketReader) 17 | 18 | func (f readerOptionFunc) apply(r *PacketReader) { 19 | f(r) 20 | } 21 | 22 | // WithMaxPacketLength returns a ReaderOption that configures a maximum packet 23 | // length on the Reader. 24 | func WithMaxPacketLength(bytes uint32) ReaderOption { 25 | return readerOptionFunc(func(r *PacketReader) { 26 | r.maxPacketLength = bytes 27 | }) 28 | } 29 | 30 | type reader interface { 31 | io.Reader 32 | io.ByteReader 33 | } 34 | 35 | // PacketReader reads MQTT packets. 36 | type PacketReader struct { 37 | maxPacketLength uint32 38 | r reader 39 | protocol byte 40 | mu sync.Mutex 41 | nRead uint32 42 | header FixedHeader 43 | packet Packet 44 | err error 45 | } 46 | 47 | // SetProtocol sets the MQTT protocol version. 48 | func (r *PacketReader) SetProtocol(protocol byte) { 49 | r.mu.Lock() 50 | r.protocol = protocol 51 | r.mu.Unlock() 52 | } 53 | 54 | // NewReader returns a new Reader on top of the given io.Reader. 55 | func NewReader(rd io.Reader, opts ...ReaderOption) *PacketReader { 56 | pr := &PacketReader{ 57 | protocol: DefaultProtocolVersion, 58 | } 59 | if r, ok := rd.(reader); ok { 60 | pr.r = r 61 | } else { 62 | pr.r = bufio.NewReader(rd) 63 | } 64 | for _, opt := range opts { 65 | opt.apply(pr) 66 | } 67 | return pr 68 | } 69 | 70 | var errUnknownPacket = NewReasonCodeError(ProtocolError, "mqtt: unknown packet") 71 | 72 | func (r *PacketReader) readVariableHeader() { 73 | switch r.packet.PacketType() { 74 | case CONNECT: 75 | r.readConnectHeader() 76 | r.protocol = r.packet.(*ConnectPacket).ConnectHeader.ProtocolVersion 77 | case CONNACK: 78 | r.readConnackHeader() 79 | case PUBLISH: 80 | r.readPublishHeader() 81 | case PUBACK: 82 | r.readPubackHeader() 83 | case PUBREC: 84 | r.readPubrecHeader() 85 | case PUBREL: 86 | r.readPubrelHeader() 87 | case PUBCOMP: 88 | r.readPubcompHeader() 89 | case SUBSCRIBE: 90 | r.readSubscribeHeader() 91 | case SUBACK: 92 | r.readSubackHeader() 93 | case UNSUBSCRIBE: 94 | r.readUnsubscribeHeader() 95 | case UNSUBACK: 96 | r.readUnsubackHeader() 97 | case DISCONNECT: 98 | r.readDisconnectHeader() 99 | case AUTH: 100 | if r.protocol < 5 { 101 | r.err = errUnknownPacket 102 | return 103 | } 104 | r.readAuthHeader() 105 | } 106 | } 107 | 108 | var errRemainingData = NewReasonCodeError(ProtocolError, "mqtt: unexpected remaining data after reading packet") 109 | 110 | func (r *PacketReader) readPayload() { 111 | switch r.packet.PacketType() { 112 | case CONNECT: 113 | r.readConnectPayload() 114 | case PUBLISH: 115 | r.readPublishPayload() 116 | case SUBSCRIBE: 117 | r.readSubscribePayload() 118 | case SUBACK: 119 | r.readSubackPayload() 120 | case UNSUBSCRIBE: 121 | r.readUnsubscribePayload() 122 | case UNSUBACK: 123 | r.readUnsubackPayload() 124 | default: 125 | if r.remaining() > 0 { 126 | r.err = errRemainingData 127 | } 128 | } 129 | } 130 | 131 | // ReadPacket reads the next packet. 132 | func (r *PacketReader) ReadPacket() (Packet, error) { 133 | r.mu.Lock() 134 | defer r.mu.Unlock() 135 | r.nRead = 0 136 | r.readFixedHeader() 137 | if r.err != nil { 138 | return nil, r.err 139 | } 140 | switch r.header.PacketType() { 141 | case CONNECT: 142 | r.packet = new(ConnectPacket) 143 | case CONNACK: 144 | r.packet = new(ConnackPacket) 145 | case PUBLISH: 146 | packet := new(PublishPacket) 147 | packet.PublishFlags = PublishFlags(r.header.typeAndFlags) & 0xf 148 | r.packet = packet 149 | case PUBACK: 150 | r.packet = new(PubackPacket) 151 | case PUBREC: 152 | r.packet = new(PubrecPacket) 153 | case PUBREL: 154 | r.packet = new(PubrelPacket) 155 | case PUBCOMP: 156 | r.packet = new(PubcompPacket) 157 | case SUBSCRIBE: 158 | r.packet = new(SubscribePacket) 159 | case SUBACK: 160 | r.packet = new(SubackPacket) 161 | case UNSUBSCRIBE: 162 | r.packet = new(UnsubscribePacket) 163 | case UNSUBACK: 164 | r.packet = new(UnsubackPacket) 165 | case PINGREQ: 166 | r.packet = new(PingreqPacket) 167 | case PINGRESP: 168 | r.packet = new(PingrespPacket) 169 | case DISCONNECT: 170 | r.packet = new(DisconnectPacket) 171 | case AUTH: 172 | r.packet = new(AuthPacket) 173 | } 174 | r.nRead = 0 175 | r.readVariableHeader() 176 | if r.err != nil { 177 | return nil, r.err 178 | } 179 | r.readPacketProperties() 180 | if r.err != nil { 181 | return nil, r.err 182 | } 183 | r.readPayload() 184 | if r.err != nil { 185 | return nil, r.err 186 | } 187 | return r.packet, nil 188 | } 189 | 190 | var errInsufficientRemainingBytes = NewReasonCodeError(MalformedPacket, "insufficient remaining bytes") 191 | 192 | func (r *PacketReader) read(b []byte) error { 193 | if r.remaining() < uint32(len(b)) { 194 | return errInsufficientRemainingBytes 195 | } 196 | n, err := io.ReadFull(r.r, b) 197 | if err != nil { 198 | return err 199 | } 200 | r.nRead += uint32(n) 201 | return nil 202 | } 203 | 204 | func (r *PacketReader) readByte() (b byte, err error) { 205 | if r.remaining() < 1 { 206 | return 0, errInsufficientRemainingBytes 207 | } 208 | b, err = r.r.ReadByte() 209 | if err != nil { 210 | return 0, err 211 | } 212 | r.nRead++ 213 | return 214 | } 215 | 216 | func (r *PacketReader) readUint16() (uint16, error) { 217 | var b [2]byte 218 | err := r.read(b[:]) 219 | if err != nil { 220 | return 0, err 221 | } 222 | return binary.BigEndian.Uint16(b[:]), nil 223 | } 224 | 225 | func (r *PacketReader) readUint32() (uint32, error) { 226 | var b [4]byte 227 | err := r.read(b[:]) 228 | if err != nil { 229 | return 0, err 230 | } 231 | return binary.BigEndian.Uint32(b[:]), nil 232 | } 233 | 234 | type countingByteReader struct { 235 | *PacketReader 236 | } 237 | 238 | func (r *PacketReader) readUvarint() (i uint64, err error) { 239 | return binary.ReadUvarint(countingByteReader{r}) 240 | } 241 | 242 | func (r countingByteReader) ReadByte() (byte, error) { 243 | return r.PacketReader.readByte() 244 | } 245 | 246 | func (r *PacketReader) readBytes() ([]byte, error) { 247 | var length uint16 248 | length, err := r.readUint16() 249 | if err != nil { 250 | return nil, err 251 | } 252 | if length == 0 { 253 | return nil, nil 254 | } 255 | b := make([]byte, length) 256 | err = r.read(b) 257 | if err != nil { 258 | return nil, err 259 | } 260 | return b, nil 261 | } 262 | 263 | var errInvalidUTF8 = NewReasonCodeError(MalformedPacket, "mqtt: invalid utf-8 string") 264 | 265 | func (r *PacketReader) readString() ([]byte, error) { 266 | b, err := r.readBytes() 267 | if err != nil { 268 | return nil, err 269 | } 270 | if r.protocol >= 4 && !utf8.Valid(b) { 271 | return nil, errInvalidUTF8 272 | } 273 | return b, nil 274 | } 275 | 276 | func (r *PacketReader) readStringPair() (k, v []byte, err error) { 277 | k, err = r.readString() 278 | if err != nil { 279 | return nil, nil, err 280 | } 281 | v, err = r.readString() 282 | if err != nil { 283 | return nil, nil, err 284 | } 285 | return k, v, nil 286 | } 287 | 288 | func (r *PacketReader) remaining() uint32 { 289 | return r.header.remainingLength - r.nRead 290 | } 291 | 292 | func (r *PacketReader) readRemaining() ([]byte, error) { 293 | b := make([]byte, int(r.remaining())) 294 | err := r.read(b) 295 | if err != nil { 296 | return nil, err 297 | } 298 | return b, nil 299 | } 300 | -------------------------------------------------------------------------------- /readwrite_test.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestReadWrite(t *testing.T) { 12 | tests := []struct { 13 | packet Packet 14 | }{ 15 | {&ConnectPacket{ 16 | ConnectHeader: ConnectHeader{ 17 | ProtocolName: []byte("MQTT"), 18 | ProtocolVersion: 4, 19 | }, 20 | ConnectPayload: ConnectPayload{ 21 | ClientIdentifier: []byte("foo"), 22 | }, 23 | }}, 24 | {&ConnackPacket{}}, 25 | {&PublishPacket{ 26 | PublishHeader: PublishHeader{ 27 | TopicName: []byte("foo"), 28 | }, 29 | PublishPayload: []byte("foo"), 30 | }}, 31 | {&PubackPacket{}}, 32 | {&PubrecPacket{}}, 33 | {&PubrelPacket{}}, 34 | {&PubcompPacket{}}, 35 | {&SubscribePacket{ 36 | SubscribePayload: []Subscription{ 37 | {[]byte("foo"), QoS2}, 38 | }, 39 | }}, 40 | {&SubackPacket{ 41 | SubackPayload: []ReasonCode{ 42 | ReasonCode(QoS2), 43 | }, 44 | }}, 45 | {&UnsubscribePacket{ 46 | UnsubscribePayload: []TopicFilter{ 47 | []byte("foo"), 48 | }, 49 | }}, 50 | {&UnsubackPacket{}}, 51 | {&PingreqPacket{}}, 52 | {&PingrespPacket{}}, 53 | {&DisconnectPacket{}}, 54 | {&AuthPacket{}}, 55 | } 56 | 57 | for _, protocol := range []byte{3, 4, 5} { 58 | for _, test := range tests { 59 | t.Run(fmt.Sprintf("MQTT%d_%T", protocol, test.packet), func(t *testing.T) { 60 | assert := assert.New(t) 61 | 62 | buf := &bytes.Buffer{} 63 | w := NewWriter(buf) 64 | w.SetProtocol(protocol) 65 | 66 | testPacket := test.packet 67 | if connectPacket, ok := testPacket.(*ConnectPacket); ok { 68 | connectPacket.ConnectHeader.ProtocolVersion = protocol 69 | connectPacket.SetUsername([]byte("username")) 70 | connectPacket.SetPassword([]byte("password")) 71 | var props Properties 72 | if protocol >= 5 { 73 | props = Properties{ 74 | {Identifier: WillDelayInterval, UintValue: 10}, 75 | } 76 | } 77 | connectPacket.SetWill(props, []byte("will-topic"), []byte("will-message")) 78 | testPacket = connectPacket 79 | } 80 | 81 | err := w.WritePacket(testPacket) 82 | if !assert.NoError(err) { 83 | t.FailNow() 84 | } 85 | 86 | t.Logf("%x", buf.Bytes()) 87 | 88 | buf = bytes.NewBuffer(buf.Bytes()) 89 | 90 | r := NewReader(buf) 91 | r.SetProtocol(protocol) 92 | 93 | pkt, err := r.ReadPacket() 94 | 95 | if _, ok := testPacket.(*AuthPacket); ok && protocol < 5 { 96 | assert.Error(err) 97 | return 98 | } 99 | 100 | if !assert.NoError(err) { 101 | t.FailNow() 102 | } 103 | 104 | assert.Equal(testPacket, pkt) 105 | 106 | assert.Equal(0, buf.Len()) 107 | }) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /reason_code.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import "fmt" 4 | 5 | // ReasonCode indicates the result of an operation 6 | type ReasonCode byte 7 | 8 | // ReasonCode Values. 9 | const ( 10 | Success ReasonCode = 0x00 // Success 11 | NormalDisconnection ReasonCode = 0x00 // Normal disconnection 12 | GrantedQoS0 ReasonCode = 0x00 // Granted QoS 0 13 | GrantedQoS1 ReasonCode = 0x01 // Granted QoS 1 14 | GrantedQoS2 ReasonCode = 0x02 // Granted QoS 2 15 | DisconnectWithWillMessage ReasonCode = 0x04 // Disconnect with Will Message 16 | NoMatchingSubscribers ReasonCode = 0x10 // No matching subscribers 17 | NoSubscriptionExisted ReasonCode = 0x11 // No subscription existed 18 | ContinueAuthentication ReasonCode = 0x18 // Continue authentication 19 | ReAuthenticate ReasonCode = 0x19 // Re-authenticate 20 | UnspecifiedError ReasonCode = 0x80 // Unspecified error 21 | MalformedPacket ReasonCode = 0x81 // Malformed Packet 22 | ProtocolError ReasonCode = 0x82 // Protocol Error 23 | ImplementationSpecificError ReasonCode = 0x83 // Implementation specific error 24 | UnsupportedProtocolVersion ReasonCode = 0x84 // Unsupported Protocol Version 25 | ClientIdentifierNotValid ReasonCode = 0x85 // Client Identifier not valid 26 | BadUsernameOrPassword ReasonCode = 0x86 // Bad User Name or Password 27 | NotAuthorized ReasonCode = 0x87 // Not authorized 28 | ServerUnavailable ReasonCode = 0x88 // Server unavailable 29 | ServerBusy ReasonCode = 0x89 // Server busy 30 | Banned ReasonCode = 0x8A // Banned 31 | ServerShuttingDown ReasonCode = 0x8B // Server shutting down 32 | BadAuthenticationMethod ReasonCode = 0x8C // Bad authentication method 33 | KeepAliveTimeout ReasonCode = 0x8D // Keep Alive timeout 34 | SessionTakenOver ReasonCode = 0x8E // Session taken over 35 | TopicFilterInvalid ReasonCode = 0x8F // Topic Filter invalid 36 | TopicNameInvalid ReasonCode = 0x90 // Topic Name invalid 37 | PacketIdentifierInUse ReasonCode = 0x91 // Packet Identifier in use 38 | PacketIdentifierNotFound ReasonCode = 0x92 // Packet Identifier not found 39 | ReceiveMaximumExceeded ReasonCode = 0x93 // Receive Maximum exceeded 40 | TopicAliasInvalid ReasonCode = 0x94 // Topic Alias invalid 41 | PacketTooLarge ReasonCode = 0x95 // Packet too large 42 | MessageRateTooHigh ReasonCode = 0x96 // Message rate too high 43 | QuotaExceeded ReasonCode = 0x97 // Quota exceeded 44 | AdministrativeAction ReasonCode = 0x98 // Administrative action 45 | PayloadFormatInvalid ReasonCode = 0x99 // Payload format invalid 46 | RetainNotSupported ReasonCode = 0x9A // Retain not supported 47 | QoSNotSupported ReasonCode = 0x9B // QoS not supported 48 | UseAnotherServer ReasonCode = 0x9C // Use another server 49 | ServerMoved ReasonCode = 0x9D // Server moved 50 | SharedSubscriptionsNotSupported ReasonCode = 0x9E // Shared Subscriptions not supported 51 | ConnectionRateExceeded ReasonCode = 0x9F // Connection rate exceeded 52 | MaximumConnectTime ReasonCode = 0xA0 // Maximum connect time 53 | SubscriptionIdentifiersNotSupported ReasonCode = 0xA1 // Subscription Identifiers not supported 54 | WildcardSubscriptionsNotSupported ReasonCode = 0xA2 // Wildcard Subscriptions not supported 55 | ) 56 | 57 | func (c ReasonCode) String() string { 58 | switch c { 59 | case 0: 60 | return "OK" // Success | Normal disconnection | Granted QoS 0 61 | case GrantedQoS1: 62 | return "Granted QoS 1" 63 | case GrantedQoS2: 64 | return "Granted QoS 2" 65 | case DisconnectWithWillMessage: 66 | return "Disconnect with Will Message" 67 | case NoMatchingSubscribers: 68 | return "No matching subscribers" 69 | case NoSubscriptionExisted: 70 | return "No subscription existed" 71 | case ContinueAuthentication: 72 | return "Continue authentication" 73 | case ReAuthenticate: 74 | return "Re-authenticate" 75 | case UnspecifiedError: 76 | return "Unspecified error" 77 | case MalformedPacket: 78 | return "Malformed Packet" 79 | case ProtocolError: 80 | return "Protocol Error" 81 | case ImplementationSpecificError: 82 | return "Implementation specific error" 83 | case UnsupportedProtocolVersion: 84 | return "Unsupported Protocol Version" 85 | case ClientIdentifierNotValid: 86 | return "Client Identifier not valid" 87 | case BadUsernameOrPassword: 88 | return "Bad User Name or Password" 89 | case NotAuthorized: 90 | return "Not authorized" 91 | case ServerUnavailable: 92 | return "Server unavailable" 93 | case ServerBusy: 94 | return "Server busy" 95 | case Banned: 96 | return "Banned" 97 | case ServerShuttingDown: 98 | return "Server shutting down" 99 | case BadAuthenticationMethod: 100 | return "Bad authentication method" 101 | case KeepAliveTimeout: 102 | return "Keep Alive timeout" 103 | case SessionTakenOver: 104 | return "Session taken over" 105 | case TopicFilterInvalid: 106 | return "Topic Filter invalid" 107 | case TopicNameInvalid: 108 | return "Topic Name invalid" 109 | case PacketIdentifierInUse: 110 | return "Packet Identifier in use" 111 | case PacketIdentifierNotFound: 112 | return "Packet Identifier not found" 113 | case ReceiveMaximumExceeded: 114 | return "Receive Maximum exceeded" 115 | case TopicAliasInvalid: 116 | return "Topic Alias invalid" 117 | case PacketTooLarge: 118 | return "Packet too large" 119 | case MessageRateTooHigh: 120 | return "Message rate too high" 121 | case QuotaExceeded: 122 | return "Quota exceeded" 123 | case AdministrativeAction: 124 | return "Administrative action" 125 | case PayloadFormatInvalid: 126 | return "Payload format invalid" 127 | case RetainNotSupported: 128 | return "Retain not supported" 129 | case QoSNotSupported: 130 | return "QoS not supported" 131 | case UseAnotherServer: 132 | return "Use another server" 133 | case ServerMoved: 134 | return "Server moved" 135 | case SharedSubscriptionsNotSupported: 136 | return "Shared Subscriptions not supported" 137 | case ConnectionRateExceeded: 138 | return "Connection rate exceeded" 139 | case MaximumConnectTime: 140 | return "Maximum connect time" 141 | case SubscriptionIdentifiersNotSupported: 142 | return "Subscription Identifiers not supported" 143 | case WildcardSubscriptionsNotSupported: 144 | return "Wildcard Subscriptions not supported" 145 | default: 146 | return fmt.Sprintf("Unknown reason code: 0x%x", byte(c)) 147 | } 148 | } 149 | 150 | // IsError returns whether the reason code is an error code. 151 | func (c ReasonCode) IsError() bool { return c >= 0x80 } 152 | 153 | type reasonCodeError struct { 154 | reasonCode ReasonCode 155 | message string 156 | } 157 | 158 | // NewReasonCodeError returns a new error based on the given reason code. 159 | func NewReasonCodeError(c ReasonCode, message string) error { 160 | if !c.IsError() { 161 | panic(fmt.Errorf("mqtt: reason code 0x%x (%q) is not an error", byte(c), c)) 162 | } 163 | return reasonCodeError{c, message} 164 | } 165 | 166 | func (e reasonCodeError) Error() string { 167 | if e.message != "" { 168 | return e.message 169 | } 170 | return fmt.Sprintf("mqtt: %s", e.reasonCode) 171 | } 172 | 173 | func (e reasonCodeError) ReasonCode() ReasonCode { 174 | return e.reasonCode 175 | } 176 | -------------------------------------------------------------------------------- /replies.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // Connack returns an ConnackPacket as response to the ConnectPacket. 4 | func (p *ConnectPacket) Connack() *ConnackPacket { 5 | var connack ConnackPacket 6 | return &connack 7 | } 8 | 9 | // Reply returns the appropriate reply to this packet. 10 | func (p *ConnectPacket) Reply() Packet { return p.Connack() } 11 | 12 | // Puback returns an PubackPacket as response to the PublishPacket. 13 | func (p *PublishPacket) Puback() *PubackPacket { 14 | var puback PubackPacket 15 | puback.PacketIdentifier = p.PacketIdentifier 16 | return &puback 17 | } 18 | 19 | // Pubrec returns an PubrecPacket as response to the PublishPacket. 20 | func (p *PublishPacket) Pubrec() *PubrecPacket { 21 | var pubrec PubrecPacket 22 | pubrec.PacketIdentifier = p.PacketIdentifier 23 | return &pubrec 24 | } 25 | 26 | // Reply returns the appropriate reply to this packet. 27 | // If no reply is needed, nil is returned. 28 | func (p *PublishPacket) Reply() Packet { 29 | switch p.QoS() { 30 | case QoS0: 31 | return nil 32 | case QoS1: 33 | return p.Puback() 34 | case QoS2: 35 | return p.Pubrec() 36 | } 37 | panic(errInvalidQoS) 38 | } 39 | 40 | // Pubrel returns an PubrelPacket as response to the PubrecPacket. 41 | func (p *PubrecPacket) Pubrel() *PubrelPacket { 42 | var pubrel PubrelPacket 43 | pubrel.PacketIdentifier = p.PacketIdentifier 44 | return &pubrel 45 | } 46 | 47 | // Reply returns the appropriate reply to this packet. 48 | func (p *PubrecPacket) Reply() Packet { return p.Pubrel() } 49 | 50 | // Pubcomp returns an PubcompPacket as response to the PubrelPacket. 51 | func (p *PubrelPacket) Pubcomp() *PubcompPacket { 52 | var pubcomp PubcompPacket 53 | pubcomp.PacketIdentifier = p.PacketIdentifier 54 | return &pubcomp 55 | } 56 | 57 | // Reply returns the appropriate reply to this packet. 58 | func (p *PubrelPacket) Reply() Packet { return p.Pubcomp() } 59 | 60 | // Suback returns an SubackPacket as response to the SubscribePacket. 61 | func (p *SubscribePacket) Suback() *SubackPacket { 62 | var suback SubackPacket 63 | suback.PacketIdentifier = p.PacketIdentifier 64 | suback.SubackPayload = make([]ReasonCode, len(p.SubscribePayload)) 65 | return &suback 66 | } 67 | 68 | // Reply returns the appropriate reply to this packet. 69 | func (p *SubscribePacket) Reply() Packet { return p.Suback() } 70 | 71 | // Unsuback returns an UnsubackPacket as response to the UnsubscribePacket. 72 | func (p *UnsubscribePacket) Unsuback() *UnsubackPacket { 73 | var unsuback UnsubackPacket 74 | unsuback.PacketIdentifier = p.PacketIdentifier 75 | unsuback.UnsubackPayload = make([]ReasonCode, len(p.UnsubscribePayload)) 76 | return &unsuback 77 | } 78 | 79 | // Reply returns the appropriate reply to this packet. 80 | func (p *UnsubscribePacket) Reply() Packet { return p.Unsuback() } 81 | 82 | // Pingresp returns an PingrespPacket as response to the PingreqPacket. 83 | func (p *PingreqPacket) Pingresp() *PingrespPacket { 84 | var pingresp PingrespPacket 85 | return &pingresp 86 | } 87 | 88 | // Reply returns the appropriate reply to this packet. 89 | func (p *PingreqPacket) Reply() Packet { return p.Pingresp() } 90 | 91 | // Auth returns an AuthPacket as response to the AuthPacket. 92 | func (p *AuthPacket) Auth() *AuthPacket { 93 | var auth AuthPacket 94 | return &auth 95 | } 96 | 97 | // Reply returns the appropriate reply to this packet. 98 | func (p *AuthPacket) Reply() Packet { return p.Auth() } 99 | -------------------------------------------------------------------------------- /suback.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | var errInvalidSubscribeReasonCode = NewReasonCodeError(ProtocolError, "mqtt: invalid subscribe reason code") 4 | 5 | func (r *PacketReader) validateSubscribeReasonCode(c ReasonCode) error { 6 | switch { 7 | case r.protocol >= 5: 8 | switch c { 9 | case GrantedQoS0, 10 | GrantedQoS1, 11 | GrantedQoS2, 12 | UnspecifiedError, 13 | ImplementationSpecificError, 14 | NotAuthorized, 15 | TopicFilterInvalid, 16 | PacketIdentifierInUse, 17 | QuotaExceeded, 18 | SharedSubscriptionsNotSupported, 19 | SubscriptionIdentifiersNotSupported, 20 | WildcardSubscriptionsNotSupported: 21 | return nil 22 | default: 23 | return errInvalidSubscribeReasonCode 24 | } 25 | default: 26 | switch c { 27 | case GrantedQoS0, 28 | GrantedQoS1, 29 | GrantedQoS2, 30 | UnspecifiedError: 31 | return nil 32 | default: 33 | return errInvalidSubscribeReasonCode 34 | } 35 | } 36 | } 37 | 38 | // SubackPacket is the Suback packet. 39 | type SubackPacket struct { 40 | SubackHeader 41 | Properties 42 | SubackPayload []ReasonCode 43 | } 44 | 45 | func (*SubackPacket) _isPacket() {} 46 | 47 | // PacketType returns the packet type of the Suback packet. 48 | func (SubackPacket) PacketType() PacketType { return SUBACK } 49 | 50 | func (p SubackPacket) fixedHeader(protocol byte) (h FixedHeader) { 51 | h.SetPacketType(SUBACK) 52 | h.remainingLength = p.size(protocol) 53 | return 54 | } 55 | 56 | func (p SubackPacket) size(protocol byte) uint32 { 57 | size := 2 58 | if protocol >= 5 { 59 | size += int(p.Properties.size()) 60 | } 61 | size += len(p.SubackPayload) 62 | return uint32(size) 63 | } 64 | 65 | // SubackHeader is the header of the Suback packet. 66 | type SubackHeader struct { 67 | PacketIdentifier uint16 68 | } 69 | 70 | func (r *PacketReader) readSubackHeader() { 71 | packet := r.packet.(*SubackPacket) 72 | if packet.SubackHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 73 | return 74 | } 75 | } 76 | 77 | func (w *PacketWriter) writeSubackHeader() { 78 | packet := w.packet.(*SubackPacket) 79 | w.err = w.writeUint16(packet.SubackHeader.PacketIdentifier) 80 | } 81 | 82 | func (r *PacketReader) readSubackPayload() { 83 | packet := r.packet.(*SubackPacket) 84 | for r.remaining() > 0 { 85 | var b byte 86 | if b, r.err = r.readByte(); r.err != nil { 87 | return 88 | } 89 | returnCode := ReasonCode(b) 90 | if r.err = r.validateSubscribeReasonCode(returnCode); r.err != nil { 91 | return 92 | } 93 | packet.SubackPayload = append(packet.SubackPayload, returnCode) 94 | } 95 | } 96 | 97 | func (w *PacketWriter) writeSubackPayload() { 98 | packet := w.packet.(*SubackPacket) 99 | switch { 100 | case w.protocol < 4: 101 | for _, returnCode := range packet.SubackPayload { 102 | if returnCode.IsError() { 103 | w.err = NewReasonCodeError(returnCode, "") 104 | return 105 | } 106 | } 107 | case w.protocol < 5: 108 | for i, returnCode := range packet.SubackPayload { 109 | if returnCode.IsError() { 110 | packet.SubackPayload[i] = UnspecifiedError 111 | } 112 | } 113 | } 114 | for _, returnCode := range packet.SubackPayload { 115 | if w.err = w.writeByte(byte(returnCode)); w.err != nil { 116 | return 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /subscribe.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // SubscribePacket is the Subscribe packet. 4 | type SubscribePacket struct { 5 | SubscribeHeader 6 | Properties 7 | SubscribePayload []Subscription 8 | } 9 | 10 | func (*SubscribePacket) _isPacket() {} 11 | 12 | // PacketType returns the packet type of the Subscribe packet. 13 | func (SubscribePacket) PacketType() PacketType { return SUBSCRIBE } 14 | 15 | func (p SubscribePacket) fixedHeader(protocol byte) (h FixedHeader) { 16 | h.SetPacketType(SUBSCRIBE) 17 | h.typeAndFlags |= 0x02 18 | h.remainingLength = p.size(protocol) 19 | return 20 | } 21 | 22 | func (p SubscribePacket) size(protocol byte) uint32 { 23 | size := 2 24 | if protocol >= 5 { 25 | size += int(p.Properties.size()) 26 | } 27 | for _, subscription := range p.SubscribePayload { 28 | size += 2 + len(subscription.TopicFilter) + 1 29 | } 30 | return uint32(size) 31 | } 32 | 33 | // SubscribeHeader is the header of the Subscribe packet. 34 | type SubscribeHeader struct { 35 | PacketIdentifier uint16 36 | } 37 | 38 | func (r *PacketReader) readSubscribeHeader() { 39 | packet := r.packet.(*SubscribePacket) 40 | if packet.SubscribeHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 41 | return 42 | } 43 | } 44 | 45 | func (w *PacketWriter) writeSubscribeHeader() { 46 | packet := w.packet.(*SubscribePacket) 47 | w.err = w.writeUint16(packet.SubscribeHeader.PacketIdentifier) 48 | } 49 | 50 | // TopicFilter is the topic filter for MQTT subscriptions. 51 | type TopicFilter []byte 52 | 53 | // Subscription is an MQTT subscription. 54 | type Subscription struct { 55 | TopicFilter TopicFilter 56 | QoS QoS 57 | } 58 | 59 | func (r *PacketReader) readSubscribePayload() { 60 | packet := r.packet.(*SubscribePacket) 61 | for r.remaining() > 0 { 62 | var subscription Subscription 63 | if subscription.TopicFilter, r.err = r.readBytes(); r.err != nil { 64 | return 65 | } 66 | var b byte 67 | if b, r.err = r.readByte(); r.err != nil { 68 | return 69 | } 70 | subscription.QoS = QoS(b) 71 | if r.err = r.validateQoS(subscription.QoS); r.err != nil { 72 | return 73 | } 74 | packet.SubscribePayload = append(packet.SubscribePayload, subscription) 75 | } 76 | } 77 | 78 | func (w *PacketWriter) writeSubscribePayload() { 79 | packet := w.packet.(*SubscribePacket) 80 | for _, subscription := range packet.SubscribePayload { 81 | if w.err = w.writeBytes(subscription.TopicFilter); w.err != nil { 82 | return 83 | } 84 | if w.err = w.writeByte(byte(subscription.QoS) & 0x03); w.err != nil { 85 | return 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /unsuback.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | var errInvalidUnsubscribeReasonCode = NewReasonCodeError(ProtocolError, "mqtt: invalid unsubscribe reason code") 4 | 5 | func (r *PacketReader) validateUnsubscribeReasonCode(c ReasonCode) error { 6 | switch { 7 | case r.protocol >= 5: 8 | switch c { 9 | case Success, 10 | NoSubscriptionExisted, 11 | UnspecifiedError, 12 | ImplementationSpecificError, 13 | NotAuthorized, 14 | TopicFilterInvalid, 15 | PacketIdentifierInUse: 16 | return nil 17 | default: 18 | return errInvalidSubscribeReasonCode 19 | } 20 | default: 21 | return nil 22 | } 23 | } 24 | 25 | // UnsubackPacket is the Unsuback packet. 26 | type UnsubackPacket struct { 27 | UnsubackHeader 28 | Properties 29 | UnsubackPayload []ReasonCode 30 | } 31 | 32 | func (*UnsubackPacket) _isPacket() {} 33 | 34 | // PacketType returns the packet type of the Unsuback packet. 35 | func (UnsubackPacket) PacketType() PacketType { return UNSUBACK } 36 | 37 | func (p UnsubackPacket) fixedHeader(protocol byte) (h FixedHeader) { 38 | h.SetPacketType(UNSUBACK) 39 | h.remainingLength = p.size(protocol) 40 | return 41 | } 42 | 43 | func (p UnsubackPacket) size(protocol byte) uint32 { 44 | size := 2 45 | if protocol >= 5 { 46 | size += len(p.UnsubackPayload) 47 | size += int(p.Properties.size()) 48 | } 49 | return uint32(size) 50 | } 51 | 52 | // UnsubackHeader is the header of the Unsuback packet. 53 | type UnsubackHeader struct { 54 | PacketIdentifier uint16 55 | } 56 | 57 | func (r *PacketReader) readUnsubackHeader() { 58 | packet := r.packet.(*UnsubackPacket) 59 | if packet.UnsubackHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 60 | return 61 | } 62 | } 63 | 64 | func (w *PacketWriter) writeUnsubackHeader() { 65 | packet := w.packet.(*UnsubackPacket) 66 | w.err = w.writeUint16(packet.UnsubackHeader.PacketIdentifier) 67 | } 68 | 69 | func (r *PacketReader) readUnsubackPayload() { 70 | packet := r.packet.(*UnsubackPacket) 71 | if r.protocol >= 5 { 72 | for r.remaining() > 0 { 73 | var b byte 74 | if b, r.err = r.readByte(); r.err != nil { 75 | return 76 | } 77 | returnCode := ReasonCode(b) 78 | if r.err = r.validateUnsubscribeReasonCode(returnCode); r.err != nil { 79 | return 80 | } 81 | packet.UnsubackPayload = append(packet.UnsubackPayload, returnCode) 82 | } 83 | } 84 | } 85 | 86 | func (w *PacketWriter) writeUnsubackPayload() { 87 | packet := w.packet.(*UnsubackPacket) 88 | if w.protocol >= 5 { 89 | for _, returnCode := range packet.UnsubackPayload { 90 | if w.err = w.writeByte(byte(returnCode)); w.err != nil { 91 | return 92 | } 93 | } 94 | } else { 95 | for _, returnCode := range packet.UnsubackPayload { 96 | if returnCode.IsError() { 97 | w.err = NewReasonCodeError(returnCode, "") 98 | return 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /unsubscribe.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | // UnsubscribePacket is the Unsubscribe packet. 4 | type UnsubscribePacket struct { 5 | UnsubscribeHeader 6 | Properties 7 | UnsubscribePayload []TopicFilter 8 | } 9 | 10 | func (*UnsubscribePacket) _isPacket() {} 11 | 12 | // PacketType returns the packet type of the Unsubscribe packet. 13 | func (UnsubscribePacket) PacketType() PacketType { return UNSUBSCRIBE } 14 | 15 | func (p UnsubscribePacket) fixedHeader(protocol byte) (h FixedHeader) { 16 | h.SetPacketType(UNSUBSCRIBE) 17 | h.typeAndFlags |= 0x02 18 | h.remainingLength = p.size(protocol) 19 | return 20 | } 21 | 22 | func (p UnsubscribePacket) size(protocol byte) uint32 { 23 | size := 2 24 | if protocol >= 5 { 25 | size += int(p.Properties.size()) 26 | } 27 | for _, topicFilter := range p.UnsubscribePayload { 28 | size += 2 + len(topicFilter) 29 | } 30 | return uint32(size) 31 | } 32 | 33 | // UnsubscribeHeader is the header of the Unsubscribe packet. 34 | type UnsubscribeHeader struct { 35 | PacketIdentifier uint16 36 | } 37 | 38 | func (r *PacketReader) readUnsubscribeHeader() { 39 | packet := r.packet.(*UnsubscribePacket) 40 | if packet.UnsubscribeHeader.PacketIdentifier, r.err = r.readUint16(); r.err != nil { 41 | return 42 | } 43 | } 44 | 45 | func (w *PacketWriter) writeUnsubscribeHeader() { 46 | packet := w.packet.(*UnsubscribePacket) 47 | w.err = w.writeUint16(packet.UnsubscribeHeader.PacketIdentifier) 48 | } 49 | 50 | func (r *PacketReader) readUnsubscribePayload() { 51 | packet := r.packet.(*UnsubscribePacket) 52 | for r.remaining() > 0 { 53 | var topicFilter TopicFilter 54 | if topicFilter, r.err = r.readBytes(); r.err != nil { 55 | return 56 | } 57 | packet.UnsubscribePayload = append(packet.UnsubscribePayload, topicFilter) 58 | } 59 | } 60 | 61 | func (w *PacketWriter) writeUnsubscribePayload() { 62 | packet := w.packet.(*UnsubscribePacket) 63 | for _, topicFilter := range packet.UnsubscribePayload { 64 | if w.err = w.writeBytes(topicFilter); w.err != nil { 65 | return 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | // WriterOption is an option for the PacketWriter. 10 | type WriterOption interface { 11 | apply(*PacketWriter) 12 | } 13 | 14 | type writerOptionFunc func(*PacketWriter) 15 | 16 | func (f writerOptionFunc) apply(w *PacketWriter) { 17 | f(w) 18 | } 19 | 20 | // PacketWriter writes MQTT packets. 21 | type PacketWriter struct { 22 | w io.Writer 23 | protocol byte 24 | mu sync.Mutex 25 | nWritten uint32 26 | packet Packet 27 | err error 28 | } 29 | 30 | // SetProtocol sets the MQTT protocol version. 31 | func (w *PacketWriter) SetProtocol(protocol byte) { 32 | w.mu.Lock() 33 | w.protocol = protocol 34 | w.mu.Unlock() 35 | } 36 | 37 | // NewWriter returns a new Writer on top of the given io.Writer. 38 | func NewWriter(wr io.Writer, opts ...WriterOption) *PacketWriter { 39 | pw := &PacketWriter{ 40 | w: wr, 41 | protocol: DefaultProtocolVersion, 42 | } 43 | for _, opt := range opts { 44 | opt.apply(pw) 45 | } 46 | return pw 47 | } 48 | 49 | func (w *PacketWriter) writeVariableHeader() { 50 | switch w.packet.PacketType() { 51 | case CONNECT: 52 | w.writeConnectHeader() 53 | case CONNACK: 54 | w.writeConnackHeader() 55 | case PUBLISH: 56 | w.writePublishHeader() 57 | case PUBACK: 58 | w.writePubackHeader() 59 | case PUBREC: 60 | w.writePubrecHeader() 61 | case PUBREL: 62 | w.writePubrelHeader() 63 | case PUBCOMP: 64 | w.writePubcompHeader() 65 | case SUBSCRIBE: 66 | w.writeSubscribeHeader() 67 | case SUBACK: 68 | w.writeSubackHeader() 69 | case UNSUBSCRIBE: 70 | w.writeUnsubscribeHeader() 71 | case UNSUBACK: 72 | w.writeUnsubackHeader() 73 | case DISCONNECT: 74 | w.writeDisconnectHeader() 75 | case AUTH: 76 | w.writeAuthHeader() 77 | } 78 | } 79 | 80 | func (w *PacketWriter) writePayload() { 81 | switch w.packet.PacketType() { 82 | case CONNECT: 83 | w.writeConnectPayload() 84 | case PUBLISH: 85 | w.writePublishPayload() 86 | case SUBSCRIBE: 87 | w.writeSubscribePayload() 88 | case SUBACK: 89 | w.writeSubackPayload() 90 | case UNSUBSCRIBE: 91 | w.writeUnsubscribePayload() 92 | case UNSUBACK: 93 | w.writeUnsubackPayload() 94 | } 95 | } 96 | 97 | // WritePacket writes the given packet. 98 | func (w *PacketWriter) WritePacket(packet Packet) error { 99 | w.mu.Lock() 100 | defer w.mu.Unlock() 101 | w.packet = packet 102 | w.writeFixedHeader() 103 | if w.err != nil { 104 | return w.err 105 | } 106 | w.writeVariableHeader() 107 | if w.err != nil { 108 | return w.err 109 | } 110 | w.writePacketProperties() 111 | if w.err != nil { 112 | return w.err 113 | } 114 | w.writePayload() 115 | if w.err != nil { 116 | return w.err 117 | } 118 | return nil 119 | } 120 | 121 | func (w *PacketWriter) write(buf []byte) error { 122 | n, err := w.w.Write(buf) 123 | if err != nil { 124 | return err 125 | } 126 | w.nWritten += uint32(n) 127 | return nil 128 | } 129 | 130 | func (w *PacketWriter) writeByte(b byte) error { 131 | return w.write([]byte{b}) 132 | } 133 | 134 | func (w *PacketWriter) writeUint16(i uint16) error { 135 | var b [2]byte 136 | binary.BigEndian.PutUint16(b[:], i) 137 | return w.write(b[:]) 138 | } 139 | 140 | func (w *PacketWriter) writeUint32(i uint32) error { 141 | var b [4]byte 142 | binary.BigEndian.PutUint32(b[:], i) 143 | return w.write(b[:]) 144 | } 145 | 146 | func (w *PacketWriter) writeUvarint(i uint32) error { 147 | var b [4]byte 148 | n := binary.PutUvarint(b[:], uint64(i)) 149 | return w.write(b[:n]) 150 | } 151 | 152 | var errInvalidBytesLength = NewReasonCodeError(ProtocolError, "mqtt: invalid bytes length") 153 | 154 | func (w *PacketWriter) writeBytes(b []byte) error { 155 | if len(b) > 65535 { 156 | return errInvalidBytesLength 157 | } 158 | err := w.writeUint16(uint16(len(b))) 159 | if err != nil { 160 | return err 161 | } 162 | return w.write(b) 163 | } 164 | 165 | func (w *PacketWriter) writeBytesPair(k, v []byte) error { 166 | if err := w.writeBytes(k); err != nil { 167 | return err 168 | } 169 | return w.writeBytes(v) 170 | } 171 | --------------------------------------------------------------------------------