├── go.mod ├── paho ├── noop_persistence.go ├── cp_utils.go ├── cp_unsubscribe.go ├── log.go ├── cp_suback.go ├── cp_unsuback.go ├── cp_pubresp.go ├── extensions │ └── rpc │ │ ├── router_test.go │ │ ├── rpc.go │ │ └── router.go ├── cp_disconnect.go ├── cp_subscribe.go ├── cmd │ ├── stdinpub │ │ └── main.go │ ├── stdoutsub │ │ └── main.go │ ├── chat │ │ └── main.go │ └── rpc │ │ └── main.go ├── cp_auth.go ├── cp_connack.go ├── message_ids.go ├── persistence.go ├── cp_publish.go ├── trace.go ├── server_test.go ├── cp_connect.go ├── client_test.go └── client.go ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── packets ├── pingreq.go ├── pingresp.go ├── auth.go ├── pubrel.go ├── unsubscribe.go ├── publish.go ├── pubcomp.go ├── subscribe.go ├── unsuback.go ├── puback.go ├── pubrec.go ├── suback.go ├── connack.go ├── connect.go ├── properties_test.go ├── disconnect.go ├── packets.go ├── packets_test.go └── properties.go ├── PULL_REQUEST_TEMPLATE.md ├── go.sum ├── README.md ├── DISTRIBUTION ├── CONTRIBUTING.md └── LICENSE /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/netdata/paho.golang 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/stretchr/testify v1.3.0 7 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 8 | ) 9 | -------------------------------------------------------------------------------- /paho/noop_persistence.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type noopPersistence struct{} 6 | 7 | func (n *noopPersistence) Open() {} 8 | 9 | func (n *noopPersistence) Put(id uint16, cp packets.ControlPacket) {} 10 | 11 | func (n *noopPersistence) Get(id uint16) packets.ControlPacket { 12 | return packets.ControlPacket{} 13 | } 14 | 15 | func (n *noopPersistence) All() []packets.ControlPacket { 16 | return nil 17 | } 18 | 19 | func (n *noopPersistence) Delete(id uint16) {} 20 | 21 | func (n *noopPersistence) Close() {} 22 | 23 | func (n *noopPersistence) Reset() {} 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To reproduce** 11 | Code and steps that reproduce the behaviour 12 | 13 | **Debug output** 14 | Any debugging output provided by the client code while reproducing the bug (either embedded to link to the output) 15 | 16 | **Expected behaviour** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Software used:** 20 | - Server was X version Y 21 | - Client version X (git sha if not a released version) 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /paho/cp_utils.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | // Byte is a helper function that take a byte and returns 4 | // a pointer to a byte of that value 5 | func Byte(b byte) *byte { 6 | return &b 7 | } 8 | 9 | // Uint32 is a helper function that take a uint32 and returns 10 | // a pointer to a uint32 of that value 11 | func Uint32(u uint32) *uint32 { 12 | return &u 13 | } 14 | 15 | // Uint16 is a helper function that take a uint16 and returns 16 | // a pointer to a uint16 of that value 17 | func Uint16(u uint16) *uint16 { 18 | return &u 19 | } 20 | 21 | // BoolToByte is a helper function that take a bool and returns 22 | // a pointer to a byte of value 1 if true or 0 if false 23 | func BoolToByte(b bool) *byte { 24 | var v byte 25 | if b { 26 | v = 1 27 | } 28 | return &v 29 | } 30 | -------------------------------------------------------------------------------- /packets/pingreq.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Pingreq is the Variable Header definition for a Pingreq control packet 10 | type Pingreq struct { 11 | } 12 | 13 | //Unpack is the implementation of the interface required function for a packet 14 | func (p *Pingreq) Unpack(r *bytes.Buffer) error { 15 | return nil 16 | } 17 | 18 | // Buffers is the implementation of the interface required function for a packet 19 | func (p *Pingreq) Buffers() net.Buffers { 20 | return nil 21 | } 22 | 23 | // WriteTo is the implementation of the interface required function for a packet 24 | func (p *Pingreq) WriteTo(w io.Writer) (int64, error) { 25 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: PINGREQ}} 26 | cp.Content = p 27 | 28 | return cp.WriteTo(w) 29 | } 30 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Do not create a Pull Request without creating an issue first** 2 | 3 | *The design of your solution to the problem should be discussed and agreed in an issue before submitting the PR* 4 | 5 | **ECA - Eclipse Contributer Agreement** 6 | 7 | Ensure you have a valid, signed [ECA](https://www.eclipse.org/legal/ECA.php) for the email address associated with your Github account (or you sign off your PR with an ECA'd address) 8 | 9 | Please provide enough information so that others can review your pull request: 10 | 11 | 12 | 13 | **Testing** 14 | 15 | Any code changes should have accompanying tests. 16 | 17 | **Closing issues** 18 | 19 | Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes. 20 | -------------------------------------------------------------------------------- /packets/pingresp.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Pingresp is the Variable Header definition for a Pingresp control packet 10 | type Pingresp struct { 11 | } 12 | 13 | //Unpack is the implementation of the interface required function for a packet 14 | func (p *Pingresp) Unpack(r *bytes.Buffer) error { 15 | return nil 16 | } 17 | 18 | // Buffers is the implementation of the interface required function for a packet 19 | func (p *Pingresp) Buffers() net.Buffers { 20 | return nil 21 | } 22 | 23 | // WriteTo is the implementation of the interface required function for a packet 24 | func (p *Pingresp) WriteTo(w io.Writer) (int64, error) { 25 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: PINGRESP}} 26 | cp.Content = p 27 | 28 | return cp.WriteTo(w) 29 | } 30 | -------------------------------------------------------------------------------- /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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 7 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 8 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= 9 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | -------------------------------------------------------------------------------- /paho/cp_unsubscribe.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Unsubscribe is a representation of an MQTT unsubscribe packet 7 | Unsubscribe struct { 8 | Topics []string 9 | Properties *UnsubscribeProperties 10 | } 11 | 12 | // UnsubscribeProperties is a struct of the properties that can be set 13 | // for a Unsubscribe packet 14 | UnsubscribeProperties struct { 15 | User map[string]string 16 | } 17 | ) 18 | 19 | // Packet returns a packets library Unsubscribe from the paho Unsubscribe 20 | // on which it is called 21 | func (u *Unsubscribe) Packet() *packets.Unsubscribe { 22 | v := &packets.Unsubscribe{Topics: u.Topics} 23 | 24 | if u.Properties != nil { 25 | v.Properties = &packets.Properties{ 26 | User: u.Properties.User, 27 | } 28 | } 29 | 30 | return v 31 | } 32 | -------------------------------------------------------------------------------- /paho/log.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/netdata/paho.golang/packets" 7 | ) 8 | 9 | type LogLevel byte 10 | 11 | const ( 12 | _ LogLevel = iota 13 | LevelTrace 14 | LevelDebug 15 | LevelWarn 16 | LevelError 17 | ) 18 | 19 | type LogEntry struct { 20 | Level LogLevel 21 | Message string 22 | Error error 23 | ControlPacket *packets.ControlPacket 24 | } 25 | 26 | func (c *Client) logCtx(ctx context.Context, level LogLevel, msg string, opts ...func(*LogEntry)) { 27 | fn := c.Logger 28 | if fn == nil { 29 | return 30 | } 31 | e := LogEntry{ 32 | Level: level, 33 | Message: msg, 34 | } 35 | for _, opt := range opts { 36 | opt(&e) 37 | } 38 | fn(ctx, e) 39 | } 40 | 41 | func (c *Client) log(level LogLevel, msg string, opts ...func(*LogEntry)) { 42 | c.logCtx(context.Background(), level, msg, opts...) 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Eclipse Paho MQTT Go client 2 | =========================== 3 | 4 | This repository contains the source code for the [Eclipse Paho](http://eclipse.org/paho) MQTT Go client library. 5 | 6 | Installation and Build 7 | ---------------------- 8 | 9 | This client is designed to work with the standard Go tools, so installation is as easy as: 10 | 11 | ```golang 12 | go get github.com/eclipse/paho.golang 13 | ``` 14 | 15 | Reporting bugs 16 | -------------- 17 | 18 | Please report bugs by raising issues for this project in github [https://github.com/eclipse/paho.golang/issues](https://github.com/eclipse/paho.golang/issues) 19 | 20 | More information 21 | ---------------- 22 | 23 | Discussion of the Paho clients takes place on the [Eclipse paho-dev mailing list](https://dev.eclipse.org/mailman/listinfo/paho-dev). 24 | 25 | General questions about the MQTT protocol are discussed in the [MQTT Google Group](https://groups.google.com/forum/?hl=en-US&fromgroups#!forum/mqtt). 26 | 27 | There is much more information available via the [MQTT community site](http://mqtt.org). 28 | -------------------------------------------------------------------------------- /paho/cp_suback.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Suback is a representation of an MQTT suback packet 7 | Suback struct { 8 | Properties *SubackProperties 9 | Reasons []byte 10 | } 11 | 12 | // SubackProperties is a struct of the properties that can be set 13 | // for a Suback packet 14 | SubackProperties struct { 15 | ReasonString string 16 | User map[string]string 17 | } 18 | ) 19 | 20 | // Packet returns a packets library Suback from the paho Suback 21 | // on which it is called 22 | func (s *Suback) Packet() *packets.Suback { 23 | return &packets.Suback{ 24 | Reasons: s.Reasons, 25 | Properties: &packets.Properties{ 26 | User: s.Properties.User, 27 | }, 28 | } 29 | } 30 | 31 | // SubackFromPacketSuback takes a packets library Suback and 32 | // returns a paho library Suback 33 | func SubackFromPacketSuback(s *packets.Suback) *Suback { 34 | return &Suback{ 35 | Reasons: s.Reasons, 36 | Properties: &SubackProperties{ 37 | ReasonString: s.Properties.ReasonString, 38 | User: s.Properties.User, 39 | }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /paho/cp_unsuback.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Unsuback is a representation of an MQTT Unsuback packet 7 | Unsuback struct { 8 | Reasons []byte 9 | Properties *UnsubackProperties 10 | } 11 | 12 | // UnsubackProperties is a struct of the properties that can be set 13 | // for a Unsuback packet 14 | UnsubackProperties struct { 15 | ReasonString string 16 | User map[string]string 17 | } 18 | ) 19 | 20 | // Packet returns a packets library Unsuback from the paho Unsuback 21 | // on which it is called 22 | func (u *Unsuback) Packet() *packets.Unsuback { 23 | return &packets.Unsuback{ 24 | Reasons: u.Reasons, 25 | Properties: &packets.Properties{ 26 | User: u.Properties.User, 27 | }, 28 | } 29 | } 30 | 31 | // UnsubackFromPacketUnsuback takes a packets library Unsuback and 32 | // returns a paho library Unsuback 33 | func UnsubackFromPacketUnsuback(u *packets.Unsuback) *Unsuback { 34 | return &Unsuback{ 35 | Reasons: u.Reasons, 36 | Properties: &UnsubackProperties{ 37 | ReasonString: u.Properties.ReasonString, 38 | User: u.Properties.User, 39 | }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packets/auth.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Auth is the Variable Header definition for a Auth control packet 10 | type Auth struct { 11 | Properties *Properties 12 | ReasonCode byte 13 | } 14 | 15 | // AuthSuccess is the return code for successful authentication 16 | const ( 17 | AuthSuccess = 0x00 18 | AuthContinueAuthentication = 0x18 19 | AuthReauthenticate = 0x19 20 | ) 21 | 22 | // Unpack is the implementation of the interface required function for a packet 23 | func (a *Auth) Unpack(r *bytes.Buffer) error { 24 | var err error 25 | 26 | success := r.Len() == 0 27 | noProps := r.Len() == 1 28 | if !success { 29 | a.ReasonCode, err = r.ReadByte() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if !noProps { 35 | err = a.Properties.Unpack(r, AUTH) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // Buffers is the implementation of the interface required function for a packet 46 | func (a *Auth) Buffers() net.Buffers { 47 | idvp := a.Properties.Pack(AUTH) 48 | propLen := encodeVBI(len(idvp)) 49 | n := net.Buffers{[]byte{a.ReasonCode}, propLen} 50 | if len(idvp) > 0 { 51 | n = append(n, idvp) 52 | } 53 | return n 54 | } 55 | 56 | // WriteTo is the implementation of the interface required function for a packet 57 | func (a *Auth) WriteTo(w io.Writer) (int64, error) { 58 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: AUTH}} 59 | cp.Content = a 60 | 61 | return cp.WriteTo(w) 62 | } 63 | -------------------------------------------------------------------------------- /packets/pubrel.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Pubrel is the Variable Header definition for a Pubrel control packet 10 | type Pubrel struct { 11 | Properties *Properties 12 | PacketID uint16 13 | ReasonCode byte 14 | } 15 | 16 | //Unpack is the implementation of the interface required function for a packet 17 | func (p *Pubrel) Unpack(r *bytes.Buffer) error { 18 | var err error 19 | success := r.Len() == 2 20 | noProps := r.Len() == 3 21 | p.PacketID, err = readUint16(r) 22 | if err != nil { 23 | return err 24 | } 25 | if !success { 26 | p.ReasonCode, err = r.ReadByte() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if !noProps { 32 | err = p.Properties.Unpack(r, PUBACK) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | } 38 | return nil 39 | } 40 | 41 | // Buffers is the implementation of the interface required function for a packet 42 | func (p *Pubrel) Buffers() net.Buffers { 43 | var b bytes.Buffer 44 | writeUint16(p.PacketID, &b) 45 | b.WriteByte(p.ReasonCode) 46 | n := net.Buffers{b.Bytes()} 47 | idvp := p.Properties.Pack(PUBREL) 48 | propLen := encodeVBI(len(idvp)) 49 | if len(idvp) > 0 { 50 | n = append(n, propLen) 51 | n = append(n, idvp) 52 | } 53 | return n 54 | } 55 | 56 | // WriteTo is the implementation of the interface required function for a packet 57 | func (p *Pubrel) WriteTo(w io.Writer) (int64, error) { 58 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBREL, Flags: 2}} 59 | cp.Content = p 60 | 61 | return cp.WriteTo(w) 62 | } 63 | -------------------------------------------------------------------------------- /DISTRIBUTION: -------------------------------------------------------------------------------- 1 | 2 | 3 | Eclipse Distribution License - v 1.0 4 | 5 | Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 | -------------------------------------------------------------------------------- /packets/unsubscribe.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Unsubscribe is the Variable Header definition for a Unsubscribe control packet 10 | type Unsubscribe struct { 11 | Topics []string 12 | Properties *Properties 13 | PacketID uint16 14 | } 15 | 16 | // Unpack is the implementation of the interface required function for a packet 17 | func (u *Unsubscribe) Unpack(r *bytes.Buffer) error { 18 | var err error 19 | u.PacketID, err = readUint16(r) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | err = u.Properties.Unpack(r, UNSUBSCRIBE) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | for { 30 | t, err := readString(r) 31 | if err != nil && err != io.EOF { 32 | return err 33 | } 34 | if err == io.EOF { 35 | break 36 | } 37 | u.Topics = append(u.Topics, t) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // Buffers is the implementation of the interface required function for a packet 44 | func (u *Unsubscribe) Buffers() net.Buffers { 45 | var b bytes.Buffer 46 | writeUint16(u.PacketID, &b) 47 | var topics bytes.Buffer 48 | for _, t := range u.Topics { 49 | writeString(t, &topics) 50 | } 51 | idvp := u.Properties.Pack(UNSUBSCRIBE) 52 | propLen := encodeVBI(len(idvp)) 53 | return net.Buffers{b.Bytes(), propLen, idvp, topics.Bytes()} 54 | } 55 | 56 | // WriteTo is the implementation of the interface required function for a packet 57 | func (u *Unsubscribe) WriteTo(w io.Writer) (int64, error) { 58 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: UNSUBSCRIBE, Flags: 2}} 59 | cp.Content = u 60 | 61 | return cp.WriteTo(w) 62 | } 63 | -------------------------------------------------------------------------------- /packets/publish.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "net" 8 | ) 9 | 10 | // Publish is the Variable Header definition for a publish control packet 11 | type Publish struct { 12 | Payload []byte 13 | Topic string 14 | Properties *Properties 15 | PacketID uint16 16 | QoS byte 17 | Duplicate bool 18 | Retain bool 19 | } 20 | 21 | //Unpack is the implementation of the interface required function for a packet 22 | func (p *Publish) Unpack(r *bytes.Buffer) error { 23 | var err error 24 | p.Topic, err = readString(r) 25 | if err != nil { 26 | return err 27 | } 28 | if p.QoS > 0 { 29 | p.PacketID, err = readUint16(r) 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | 35 | err = p.Properties.Unpack(r, PUBLISH) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | p.Payload, err = ioutil.ReadAll(r) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // Buffers is the implementation of the interface required function for a packet 49 | func (p *Publish) Buffers() net.Buffers { 50 | var b bytes.Buffer 51 | writeString(p.Topic, &b) 52 | if p.QoS > 0 { 53 | writeUint16(p.PacketID, &b) 54 | } 55 | idvp := p.Properties.Pack(PUBLISH) 56 | propLen := encodeVBI(len(idvp)) 57 | return net.Buffers{b.Bytes(), propLen, idvp, p.Payload} 58 | 59 | } 60 | 61 | // WriteTo is the implementation of the interface required function for a packet 62 | func (p *Publish) WriteTo(w io.Writer) (int64, error) { 63 | f := p.QoS << 1 64 | if p.Duplicate { 65 | f |= 1 << 3 66 | } 67 | if p.Retain { 68 | f |= 1 69 | } 70 | 71 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBLISH, Flags: f}} 72 | cp.Content = p 73 | 74 | return cp.WriteTo(w) 75 | } 76 | -------------------------------------------------------------------------------- /paho/cp_pubresp.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // PublishResponse is a generic representation of a response 7 | // to a QoS1 or QoS2 Publish 8 | PublishResponse struct { 9 | Properties *PublishResponseProperties 10 | ReasonCode byte 11 | } 12 | 13 | // PublishResponseProperties is the properties associated with 14 | // a response to a QoS1 or QoS2 Publish 15 | PublishResponseProperties struct { 16 | ReasonString string 17 | User map[string]string 18 | } 19 | ) 20 | 21 | // PublishResponseFromPuback takes a packets library Puback and 22 | // returns a paho library PublishResponse 23 | func PublishResponseFromPuback(pa *packets.Puback) *PublishResponse { 24 | return &PublishResponse{ 25 | ReasonCode: pa.ReasonCode, 26 | Properties: &PublishResponseProperties{ 27 | ReasonString: pa.Properties.ReasonString, 28 | User: pa.Properties.User, 29 | }, 30 | } 31 | } 32 | 33 | // PublishResponseFromPubcomp takes a packets library Pubcomp and 34 | // returns a paho library PublishResponse 35 | func PublishResponseFromPubcomp(pc *packets.Pubcomp) *PublishResponse { 36 | return &PublishResponse{ 37 | ReasonCode: pc.ReasonCode, 38 | Properties: &PublishResponseProperties{ 39 | ReasonString: pc.Properties.ReasonString, 40 | User: pc.Properties.User, 41 | }, 42 | } 43 | } 44 | 45 | // PublishResponseFromPubrec takes a packets library Pubrec and 46 | // returns a paho library PublishResponse 47 | func PublishResponseFromPubrec(pr *packets.Pubrec) *PublishResponse { 48 | return &PublishResponse{ 49 | ReasonCode: pr.ReasonCode, 50 | Properties: &PublishResponseProperties{ 51 | ReasonString: pr.Properties.ReasonString, 52 | User: pr.Properties.User, 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /paho/extensions/rpc/router_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_match(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | route string 12 | topic string 13 | want bool 14 | }{ 15 | {"basic1", "a/b", "a/b", true}, 16 | {"basic2", "a", "a/b", false}, 17 | {"plus1", "a/+", "a/b", true}, 18 | {"plus2", "+/b", "a/b", true}, 19 | {"plus3", "a/+/c", "a/b/c", true}, 20 | {"plus4", "a/+/c", "a/asdf/c", true}, 21 | {"hash1", "#", "a/b", true}, 22 | {"hash2", "a/#", "a/b", true}, 23 | {"hash3", "b/#", "a/b", false}, 24 | {"hash4", "#", "", true}, 25 | {"share1", "$share/a/b", "a/b", true}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if got := match(tt.route, tt.topic); got != tt.want { 30 | t.Errorf("match() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func Test_routeIncludesTopic(t *testing.T) { 37 | type args struct { 38 | route string 39 | topic string 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | want bool 45 | }{ 46 | // TODO: Add test cases. 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | if got := routeIncludesTopic(tt.args.route, tt.args.topic); got != tt.want { 51 | t.Errorf("routeIncludesTopic() = %v, want %v", got, tt.want) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func Test_routeSplit(t *testing.T) { 58 | type args struct { 59 | route string 60 | } 61 | tests := []struct { 62 | name string 63 | args args 64 | want []string 65 | }{ 66 | // TODO: Add test cases. 67 | } 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | if got := routeSplit(tt.args.route); !reflect.DeepEqual(got, tt.want) { 71 | t.Errorf("routeSplit() = %v, want %v", got, tt.want) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /paho/cp_disconnect.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Disconnect is a representation of the MQTT Disconnect packet 7 | Disconnect struct { 8 | Properties *DisconnectProperties 9 | ReasonCode byte 10 | } 11 | 12 | // DisconnectProperties is a struct of the properties that can be set 13 | // for a Disconnect packet 14 | DisconnectProperties struct { 15 | ServerReference string 16 | ReasonString string 17 | SessionExpiryInterval *uint32 18 | User map[string]string 19 | } 20 | ) 21 | 22 | // InitProperties is a function that takes a lower level 23 | // Properties struct and completes the properties of the Disconnect on 24 | // which it is called 25 | func (d *Disconnect) InitProperties(p *packets.Properties) { 26 | d.Properties = &DisconnectProperties{ 27 | SessionExpiryInterval: p.SessionExpiryInterval, 28 | ServerReference: p.ServerReference, 29 | ReasonString: p.ReasonString, 30 | User: p.User, 31 | } 32 | } 33 | 34 | // DisconnectFromPacketDisconnect takes a packets library Disconnect and 35 | // returns a paho library Disconnect 36 | func DisconnectFromPacketDisconnect(p *packets.Disconnect) *Disconnect { 37 | v := &Disconnect{ReasonCode: p.ReasonCode} 38 | v.InitProperties(p.Properties) 39 | 40 | return v 41 | } 42 | 43 | // Packet returns a packets library Disconnect from the paho Disconnect 44 | // on which it is called 45 | func (d *Disconnect) Packet() *packets.Disconnect { 46 | v := &packets.Disconnect{ReasonCode: d.ReasonCode} 47 | 48 | if d.Properties != nil { 49 | v.Properties = &packets.Properties{ 50 | SessionExpiryInterval: d.Properties.SessionExpiryInterval, 51 | ServerReference: d.Properties.ServerReference, 52 | ReasonString: d.Properties.ReasonString, 53 | User: d.Properties.User, 54 | } 55 | } 56 | 57 | return v 58 | } 59 | -------------------------------------------------------------------------------- /paho/cp_subscribe.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Subscribe is a representation of a MQTT subscribe packet 7 | Subscribe struct { 8 | Properties *SubscribeProperties 9 | Subscriptions map[string]SubscribeOptions 10 | } 11 | 12 | // SubscribeOptions is the struct representing the options for a subscription 13 | SubscribeOptions struct { 14 | QoS byte 15 | RetainHandling byte 16 | NoLocal bool 17 | RetainAsPublished bool 18 | } 19 | ) 20 | 21 | // SubscribeProperties is a struct of the properties that can be set 22 | // for a Subscribe packet 23 | type SubscribeProperties struct { 24 | SubscriptionIdentifier *uint32 25 | User map[string]string 26 | } 27 | 28 | // InitProperties is a function that takes a packet library 29 | // Properties struct and completes the properties of the Subscribe on 30 | // which it is called 31 | func (s *Subscribe) InitProperties(prop *packets.Properties) { 32 | s.Properties = &SubscribeProperties{ 33 | SubscriptionIdentifier: prop.SubscriptionIdentifier, 34 | User: prop.User, 35 | } 36 | } 37 | 38 | // PacketSubOptionsFromSubscribeOptions returns a map of string to packet 39 | // library SubOptions for the paho Subscribe on which it is called 40 | func (s *Subscribe) PacketSubOptionsFromSubscribeOptions() map[string]packets.SubOptions { 41 | r := make(map[string]packets.SubOptions) 42 | for k, v := range s.Subscriptions { 43 | r[k] = packets.SubOptions{ 44 | QoS: v.QoS, 45 | NoLocal: v.NoLocal, 46 | RetainAsPublished: v.RetainAsPublished, 47 | RetainHandling: v.RetainHandling, 48 | } 49 | } 50 | 51 | return r 52 | } 53 | 54 | // Packet returns a packets library Subscribe from the paho Subscribe 55 | // on which it is called 56 | func (s *Subscribe) Packet() *packets.Subscribe { 57 | v := &packets.Subscribe{Subscriptions: s.PacketSubOptionsFromSubscribeOptions()} 58 | 59 | if s.Properties != nil { 60 | v.Properties = &packets.Properties{ 61 | SubscriptionIdentifier: s.Properties.SubscriptionIdentifier, 62 | User: s.Properties.User, 63 | } 64 | } 65 | 66 | return v 67 | } 68 | -------------------------------------------------------------------------------- /packets/pubcomp.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Pubcomp is the Variable Header definition for a Pubcomp control packet 10 | type Pubcomp struct { 11 | Properties *Properties 12 | PacketID uint16 13 | ReasonCode byte 14 | } 15 | 16 | // PubcompSuccess, etc are the list of valid pubcomp reason codes. 17 | const ( 18 | PubcompSuccess = 0x00 19 | PubcompPacketIdentifierNotFound = 0x92 20 | ) 21 | 22 | //Unpack is the implementation of the interface required function for a packet 23 | func (p *Pubcomp) Unpack(r *bytes.Buffer) error { 24 | var err error 25 | success := r.Len() == 2 26 | noProps := r.Len() == 3 27 | p.PacketID, err = readUint16(r) 28 | if err != nil { 29 | return err 30 | } 31 | if !success { 32 | p.ReasonCode, err = r.ReadByte() 33 | if err != nil { 34 | return err 35 | } 36 | 37 | if !noProps { 38 | err = p.Properties.Unpack(r, PUBACK) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | // Buffers is the implementation of the interface required function for a packet 48 | func (p *Pubcomp) Buffers() net.Buffers { 49 | var b bytes.Buffer 50 | writeUint16(p.PacketID, &b) 51 | b.WriteByte(p.ReasonCode) 52 | n := net.Buffers{b.Bytes()} 53 | idvp := p.Properties.Pack(PUBCOMP) 54 | propLen := encodeVBI(len(idvp)) 55 | if len(idvp) > 0 { 56 | n = append(n, propLen) 57 | n = append(n, idvp) 58 | } 59 | return n 60 | } 61 | 62 | // WriteTo is the implementation of the interface required function for a packet 63 | func (p *Pubcomp) WriteTo(w io.Writer) (int64, error) { 64 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBCOMP}} 65 | cp.Content = p 66 | 67 | return cp.WriteTo(w) 68 | } 69 | 70 | // Reason returns a string representation of the meaning of the ReasonCode 71 | func (p *Pubcomp) Reason() string { 72 | switch p.ReasonCode { 73 | case 0: 74 | return "Success - Packet Identifier released. Publication of QoS 2 message is complete." 75 | case 146: 76 | return "Packet Identifier not found - The Packet Identifier is not known. This is not an error during recovery, but at other times indicates a mismatch between the Session State on the Client and Server." 77 | } 78 | 79 | return "" 80 | } 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Paho 2 | ==================== 3 | 4 | Thanks for your interest in this project. 5 | 6 | Project description: 7 | -------------------- 8 | 9 | The Paho project has been created to provide scalable open-source implementations of open and standard messaging protocols aimed at new, existing, and emerging applications for Machine-to-Machine (M2M) and Internet of Things (IoT). 10 | Paho reflects the inherent physical and cost constraints of device connectivity. Its objectives include effective levels of decoupling between devices and applications, designed to keep markets open and encourage the rapid growth of scalable Web and Enterprise middleware and applications. Paho is being kicked off with MQTT publish/subscribe client implementations for use on embedded platforms, along with corresponding server support as determined by the community. 11 | 12 | - https://projects.eclipse.org/projects/technology.paho 13 | 14 | Developer resources: 15 | -------------------- 16 | 17 | Information regarding source code management, builds, coding standards, and more. 18 | 19 | - https://projects.eclipse.org/projects/technology.paho/developer 20 | 21 | Contributor License Agreement: 22 | ------------------------------ 23 | 24 | Before your contribution can be accepted by the project, you need to create and electronically sign the Eclipse Foundation Contributor License Agreement (CLA). 25 | 26 | - http://www.eclipse.org/legal/CLA.php 27 | 28 | Contributing Code: 29 | ------------------ 30 | 31 | The Go client is developed in Github, see their documentation on the process of forking and pull requests; https://help.github.com/categories/collaborating-on-projects-using-pull-requests/ 32 | 33 | Git commit messages should follow the style described here; 34 | 35 | http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 36 | 37 | Contact: 38 | -------- 39 | 40 | Contact the project developers via the project's "dev" list. 41 | 42 | - https://dev.eclipse.org/mailman/listinfo/paho-dev 43 | 44 | Search for bugs: 45 | ---------------- 46 | 47 | This project uses Github issues to track ongoing development and issues. 48 | 49 | - https://github.com/eclipse/paho.golang/issues 50 | 51 | Create a new bug: 52 | ----------------- 53 | 54 | Be sure to search for existing bugs before you create another one. Remember that contributions are always welcome! 55 | 56 | - https://github.com/eclipse/paho.golang/issues 57 | -------------------------------------------------------------------------------- /paho/cmd/stdinpub/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "syscall" 14 | 15 | "github.com/netdata/paho.golang/paho" 16 | ) 17 | 18 | func main() { 19 | stdin := bufio.NewReader(os.Stdin) 20 | hostname, _ := os.Hostname() 21 | 22 | server := flag.String("server", "127.0.0.1:1883", "The full URL of the MQTT server to connect to") 23 | topic := flag.String("topic", hostname, "Topic to publish the messages on") 24 | qos := flag.Int("qos", 0, "The QoS to send the messages at") 25 | retained := flag.Bool("retained", false, "Are the messages sent with the retained flag") 26 | clientid := flag.String("clientid", "", "A clientid for the connection") 27 | username := flag.String("username", "", "A username to authenticate to the MQTT server") 28 | password := flag.String("password", "", "Password to match username") 29 | flag.Parse() 30 | 31 | conn, err := net.Dial("tcp", *server) 32 | if err != nil { 33 | log.Fatalf("Failed to connect to %s: %s", *server, err) 34 | } 35 | 36 | c := paho.NewClient(paho.ClientConfig{ 37 | Conn: conn, 38 | }) 39 | 40 | cp := &paho.Connect{ 41 | KeepAlive: 30, 42 | ClientID: *clientid, 43 | CleanStart: true, 44 | Username: *username, 45 | Password: []byte(*password), 46 | } 47 | 48 | if *username != "" { 49 | cp.UsernameFlag = true 50 | } 51 | if *password != "" { 52 | cp.PasswordFlag = true 53 | } 54 | 55 | log.Println(cp.UsernameFlag, cp.PasswordFlag) 56 | 57 | ca, err := c.Connect(context.Background(), cp) 58 | if err != nil { 59 | log.Fatalln(err) 60 | } 61 | if ca.ReasonCode != 0 { 62 | log.Fatalf("Failed to connect to %s : %d - %s", *server, ca.ReasonCode, ca.Properties.ReasonString) 63 | } 64 | 65 | fmt.Printf("Connected to %s\n", *server) 66 | 67 | ic := make(chan os.Signal, 1) 68 | signal.Notify(ic, os.Interrupt, syscall.SIGTERM) 69 | go func() { 70 | <-ic 71 | fmt.Println("signal received, exiting") 72 | if c != nil { 73 | d := &paho.Disconnect{ReasonCode: 0} 74 | c.Disconnect(d) 75 | } 76 | os.Exit(0) 77 | }() 78 | 79 | for { 80 | message, err := stdin.ReadString('\n') 81 | if err == io.EOF { 82 | os.Exit(0) 83 | } 84 | 85 | if _, err = c.Publish(context.Background(), &paho.Publish{ 86 | Topic: *topic, 87 | QoS: byte(*qos), 88 | Retain: *retained, 89 | Payload: []byte(message), 90 | }); err != nil { 91 | log.Println(err) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /paho/cmd/stdoutsub/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | 13 | "github.com/netdata/paho.golang/paho" 14 | ) 15 | 16 | func main() { 17 | server := flag.String("server", "127.0.0.1:1883", "The MQTT server to connect to ex: 127.0.0.1:1883") 18 | topic := flag.String("topic", "#", "Topic to subscribe to") 19 | qos := flag.Int("qos", 0, "The QoS to subscribe to messages at") 20 | clientid := flag.String("clientid", "", "A clientid for the connection") 21 | username := flag.String("username", "", "A username to authenticate to the MQTT server") 22 | password := flag.String("password", "", "Password to match username") 23 | flag.Parse() 24 | 25 | logger := log.New(os.Stdout, "SUB: ", log.LstdFlags) 26 | 27 | paho.SetDebugLogger(logger) 28 | paho.SetErrorLogger(logger) 29 | msgChan := make(chan *paho.Publish) 30 | 31 | conn, err := net.Dial("tcp", *server) 32 | if err != nil { 33 | log.Fatalf("Failed to connect to %s: %s", *server, err) 34 | } 35 | 36 | c := paho.NewClient(paho.ClientConfig{ 37 | Router: paho.NewSingleHandlerRouter(func(m *paho.Publish) { 38 | msgChan <- m 39 | }), 40 | Conn: conn, 41 | }) 42 | 43 | cp := &paho.Connect{ 44 | KeepAlive: 30, 45 | ClientID: *clientid, 46 | CleanStart: true, 47 | Username: *username, 48 | Password: []byte(*password), 49 | } 50 | 51 | if *username != "" { 52 | cp.UsernameFlag = true 53 | } 54 | if *password != "" { 55 | cp.PasswordFlag = true 56 | } 57 | 58 | ca, err := c.Connect(context.Background(), cp) 59 | if err != nil { 60 | log.Fatalln(err) 61 | } 62 | if ca.ReasonCode != 0 { 63 | log.Fatalf("Failed to connect to %s : %d - %s", *server, ca.ReasonCode, ca.Properties.ReasonString) 64 | } 65 | 66 | fmt.Printf("Connected to %s\n", *server) 67 | 68 | ic := make(chan os.Signal, 1) 69 | signal.Notify(ic, os.Interrupt, syscall.SIGTERM) 70 | go func() { 71 | <-ic 72 | fmt.Println("signal received, exiting") 73 | if c != nil { 74 | d := &paho.Disconnect{ReasonCode: 0} 75 | c.Disconnect(d) 76 | } 77 | os.Exit(0) 78 | }() 79 | 80 | sa, err := c.Subscribe(context.Background(), &paho.Subscribe{ 81 | Subscriptions: map[string]paho.SubscribeOptions{ 82 | *topic: paho.SubscribeOptions{QoS: byte(*qos)}, 83 | }, 84 | }) 85 | if err != nil { 86 | log.Fatalln(err) 87 | } 88 | if sa.Reasons[0] != byte(*qos) { 89 | log.Fatalf("Failed to subscribe to %s : %d", *topic, sa.Reasons[0]) 90 | } 91 | log.Printf("Subscribed to %s", *topic) 92 | 93 | for m := range msgChan { 94 | log.Println("Received message:", string(m.Payload)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /paho/cp_auth.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Auth is a representation of the MQTT Auth packet 7 | Auth struct { 8 | Properties *AuthProperties 9 | ReasonCode byte 10 | } 11 | 12 | // AuthProperties is a struct of the properties that can be set 13 | // for a Auth packet 14 | AuthProperties struct { 15 | AuthData []byte 16 | AuthMethod string 17 | ReasonString string 18 | User map[string]string 19 | } 20 | ) 21 | 22 | // InitProperties is a function that takes a lower level 23 | // Properties struct and completes the properties of the Auth on 24 | // which it is called 25 | func (a *Auth) InitProperties(p *packets.Properties) { 26 | a.Properties = &AuthProperties{ 27 | AuthMethod: p.AuthMethod, 28 | AuthData: p.AuthData, 29 | ReasonString: p.ReasonString, 30 | User: p.User, 31 | } 32 | } 33 | 34 | // AuthFromPacketAuth takes a packets library Auth and 35 | // returns a paho library Auth 36 | func AuthFromPacketAuth(a *packets.Auth) *Auth { 37 | v := &Auth{ReasonCode: a.ReasonCode} 38 | v.InitProperties(a.Properties) 39 | 40 | return v 41 | } 42 | 43 | // Packet returns a packets library Auth from the paho Auth 44 | // on which it is called 45 | func (a *Auth) Packet() *packets.Auth { 46 | v := &packets.Auth{ReasonCode: a.ReasonCode} 47 | 48 | if a.Properties != nil { 49 | v.Properties = &packets.Properties{ 50 | AuthMethod: a.Properties.AuthMethod, 51 | AuthData: a.Properties.AuthData, 52 | ReasonString: a.Properties.ReasonString, 53 | User: a.Properties.User, 54 | } 55 | } 56 | 57 | return v 58 | } 59 | 60 | // AuthResponse is a represenation of the response to an Auth 61 | // packet 62 | type AuthResponse struct { 63 | Properties *AuthProperties 64 | ReasonCode byte 65 | Success bool 66 | } 67 | 68 | // AuthResponseFromPacketAuth takes a packets library Auth and 69 | // returns a paho library AuthResponse 70 | func AuthResponseFromPacketAuth(a *packets.Auth) *AuthResponse { 71 | return &AuthResponse{ 72 | Success: true, 73 | ReasonCode: a.ReasonCode, 74 | Properties: &AuthProperties{ 75 | ReasonString: a.Properties.ReasonString, 76 | User: a.Properties.User, 77 | }, 78 | } 79 | } 80 | 81 | // AuthResponseFromPacketDisconnect takes a packets library Disconnect and 82 | // returns a paho library AuthResponse 83 | func AuthResponseFromPacketDisconnect(d *packets.Disconnect) *AuthResponse { 84 | return &AuthResponse{ 85 | Success: true, 86 | ReasonCode: d.ReasonCode, 87 | Properties: &AuthProperties{ 88 | ReasonString: d.Properties.ReasonString, 89 | User: d.Properties.User, 90 | }, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /paho/cp_connack.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Connack is a representation of the MQTT Connack packet 7 | Connack struct { 8 | Properties *ConnackProperties 9 | ReasonCode byte 10 | SessionPresent bool 11 | } 12 | 13 | // ConnackProperties is a struct of the properties that can be set 14 | // for a Connack packet 15 | ConnackProperties struct { 16 | AuthData []byte 17 | AuthMethod string 18 | ResponseInfo string 19 | ServerReference string 20 | ReasonString string 21 | AssignedClientID string 22 | MaximumPacketSize *uint32 23 | ReceiveMaximum *uint16 24 | TopicAliasMaximum *uint16 25 | ServerKeepAlive *uint16 26 | MaximumQoS *byte 27 | User map[string]string 28 | WildcardSubAvailable bool 29 | SubIDAvailable bool 30 | SharedSubAvailable bool 31 | RetainAvailable bool 32 | } 33 | ) 34 | 35 | // InitProperties is a function that takes a lower level 36 | // Properties struct and completes the properties of the Connack on 37 | // which it is called 38 | func (c *Connack) InitProperties(p *packets.Properties) { 39 | c.Properties = &ConnackProperties{ 40 | AssignedClientID: p.AssignedClientID, 41 | ServerKeepAlive: p.ServerKeepAlive, 42 | WildcardSubAvailable: true, 43 | SubIDAvailable: true, 44 | SharedSubAvailable: true, 45 | RetainAvailable: true, 46 | ResponseInfo: p.ResponseInfo, 47 | AuthMethod: p.AuthMethod, 48 | AuthData: p.AuthData, 49 | ServerReference: p.ServerReference, 50 | ReasonString: p.ReasonString, 51 | ReceiveMaximum: p.ReceiveMaximum, 52 | TopicAliasMaximum: p.TopicAliasMaximum, 53 | MaximumQoS: p.MaximumQOS, 54 | MaximumPacketSize: p.MaximumPacketSize, 55 | User: p.User, 56 | } 57 | 58 | if p.WildcardSubAvailable != nil { 59 | c.Properties.WildcardSubAvailable = *p.WildcardSubAvailable == 1 60 | } 61 | if p.SubIDAvailable != nil { 62 | c.Properties.SubIDAvailable = *p.SubIDAvailable == 1 63 | } 64 | if p.SharedSubAvailable != nil { 65 | c.Properties.SharedSubAvailable = *p.SharedSubAvailable == 1 66 | } 67 | if p.RetainAvailable != nil { 68 | c.Properties.RetainAvailable = *p.RetainAvailable == 1 69 | } 70 | } 71 | 72 | // ConnackFromPacketConnack takes a packets library Connack and 73 | // returns a paho library Connack 74 | func ConnackFromPacketConnack(c *packets.Connack) *Connack { 75 | v := &Connack{ 76 | SessionPresent: c.SessionPresent, 77 | ReasonCode: c.ReasonCode, 78 | } 79 | v.InitProperties(c.Properties) 80 | 81 | return v 82 | } 83 | -------------------------------------------------------------------------------- /paho/extensions/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/netdata/paho.golang/paho" 10 | ) 11 | 12 | // Handler is the struct providing a request/response functionality for the paho 13 | // MQTT v5 client 14 | type Handler struct { 15 | sync.Mutex 16 | c *paho.Client 17 | clientID string 18 | correlData map[string]chan *paho.Publish 19 | } 20 | 21 | func NewHandler(config paho.ClientConfig, clientID string) (*Handler, error) { 22 | r, ok := config.Router.(Router) 23 | if !ok && config.Router != nil { 24 | return nil, fmt.Errorf("config.Router must be nil or implement rpc.Router") 25 | } 26 | if r == nil { 27 | r = NewStandardRouter() 28 | config.Router = r 29 | } 30 | 31 | c := paho.NewClient(config) 32 | h := &Handler{ 33 | c: c, 34 | correlData: make(map[string]chan *paho.Publish), 35 | clientID: clientID, 36 | } 37 | 38 | r.RegisterHandler(fmt.Sprintf("%s/responses", clientID), h.responseHandler) 39 | 40 | _, err := c.Subscribe(context.Background(), &paho.Subscribe{ 41 | Subscriptions: map[string]paho.SubscribeOptions{ 42 | fmt.Sprintf("%s/responses", clientID): paho.SubscribeOptions{QoS: 1}, 43 | }, 44 | }) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return h, nil 50 | } 51 | 52 | func (h *Handler) addCorrelID(cID string, r chan *paho.Publish) { 53 | h.Lock() 54 | defer h.Unlock() 55 | 56 | h.correlData[cID] = r 57 | } 58 | 59 | func (h *Handler) getCorrelIDChan(cID string) chan *paho.Publish { 60 | h.Lock() 61 | defer h.Unlock() 62 | 63 | rChan := h.correlData[cID] 64 | delete(h.correlData, cID) 65 | 66 | return rChan 67 | } 68 | 69 | func (h *Handler) Request(pb *paho.Publish) (*paho.Publish, error) { 70 | cID := fmt.Sprintf("%d", time.Now().UnixNano()) 71 | rChan := make(chan *paho.Publish) 72 | 73 | h.addCorrelID(cID, rChan) 74 | 75 | if pb.Properties == nil { 76 | pb.Properties = &paho.PublishProperties{} 77 | } 78 | 79 | pb.Properties.CorrelationData = []byte(cID) 80 | pb.Properties.ResponseTopic = fmt.Sprintf("%s/responses", h.clientID) 81 | pb.Retain = false 82 | 83 | _, err := h.c.Publish(context.Background(), pb) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | resp := <-rChan 89 | return resp, nil 90 | } 91 | 92 | func (h *Handler) responseHandler(pb *paho.Publish, ack func() error) { 93 | defer ack() 94 | if pb.Properties == nil || pb.Properties.CorrelationData == nil { 95 | return 96 | } 97 | 98 | rChan := h.getCorrelIDChan(string(pb.Properties.CorrelationData)) 99 | if rChan == nil { 100 | return 101 | } 102 | 103 | rChan <- pb 104 | } 105 | -------------------------------------------------------------------------------- /packets/subscribe.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Subscribe is the Variable Header definition for a Subscribe control packet 10 | type Subscribe struct { 11 | Properties *Properties 12 | Subscriptions map[string]SubOptions 13 | PacketID uint16 14 | } 15 | 16 | // SubOptions is the struct representing the options for a subscription 17 | type SubOptions struct { 18 | QoS byte 19 | RetainHandling byte 20 | NoLocal bool 21 | RetainAsPublished bool 22 | } 23 | 24 | // Pack is the implementation of the interface required function for a packet 25 | func (s *SubOptions) Pack() byte { 26 | var ret byte 27 | ret |= s.QoS & 0x03 28 | if s.NoLocal { 29 | ret |= 1 << 2 30 | } 31 | if s.RetainAsPublished { 32 | ret |= 1 << 3 33 | } 34 | ret |= s.RetainHandling & 0x30 35 | 36 | return ret 37 | } 38 | 39 | // Unpack is the implementation of the interface required function for a packet 40 | func (s *SubOptions) Unpack(r *bytes.Buffer) error { 41 | b, err := r.ReadByte() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | s.QoS = b & 0x03 47 | s.NoLocal = (b & 1 << 2) == 1 48 | s.RetainAsPublished = (b & 1 << 3) == 1 49 | s.RetainHandling = b & 0x30 50 | 51 | return nil 52 | } 53 | 54 | // Unpack is the implementation of the interface required function for a packet 55 | func (s *Subscribe) Unpack(r *bytes.Buffer) error { 56 | var err error 57 | s.PacketID, err = readUint16(r) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | err = s.Properties.Unpack(r, SUBSCRIBE) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | for r.Len() > 0 { 68 | var so SubOptions 69 | t, err := readString(r) 70 | if err != nil { 71 | return err 72 | } 73 | if err = so.Unpack(r); err != nil { 74 | return err 75 | } 76 | s.Subscriptions[t] = so 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // Buffers is the implementation of the interface required function for a packet 83 | func (s *Subscribe) Buffers() net.Buffers { 84 | var b bytes.Buffer 85 | writeUint16(s.PacketID, &b) 86 | var subs bytes.Buffer 87 | for t, o := range s.Subscriptions { 88 | writeString(t, &subs) 89 | subs.WriteByte(o.Pack()) 90 | } 91 | idvp := s.Properties.Pack(SUBSCRIBE) 92 | propLen := encodeVBI(len(idvp)) 93 | return net.Buffers{b.Bytes(), propLen, idvp, subs.Bytes()} 94 | } 95 | 96 | // WriteTo is the implementation of the interface required function for a packet 97 | func (s *Subscribe) WriteTo(w io.Writer) (int64, error) { 98 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: SUBSCRIBE, Flags: 2}} 99 | cp.Content = s 100 | 101 | return cp.WriteTo(w) 102 | } 103 | -------------------------------------------------------------------------------- /paho/message_ids.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | 8 | "github.com/netdata/paho.golang/packets" 9 | ) 10 | 11 | // ErrNoMoreIDs is an error returned when there are no more available packet IDs. 12 | var ErrNoMoreIDs = errors.New("no more packet ids available.") 13 | 14 | // MIDService defines the interface for a struct that handles the 15 | // relationship between message ids and CPContexts 16 | // Request() takes a *CPContext and returns a uint16 that is the 17 | // messageid that should be used by the code that called Request() 18 | // Get() takes a uint16 that is a messageid and returns the matching 19 | // *CPContext that the MIDService has associated with that messageid 20 | // Free() takes a uint16 that is a messageid and instructs the MIDService 21 | // to mark that messageid as available for reuse 22 | // Clear() resets the internal state of the MIDService 23 | type MIDService interface { 24 | Request(*CPContext) (uint16, error) 25 | Get(uint16) *CPContext 26 | Free(uint16) 27 | Clear() 28 | } 29 | 30 | // CPContext is the struct that is used to return responses to 31 | // ControlPackets that have them, eg: the suback to a subscribe. 32 | // The response packet is send down the Return channel and the 33 | // Context is used to track timeouts. 34 | type CPContext struct { 35 | Context context.Context 36 | Return chan packets.ControlPacket 37 | } 38 | 39 | // MIDs is the default MIDService provided by this library. 40 | // It uses a map of uint16 to *CPContext to track responses 41 | // to messages with a messageid 42 | type MIDs struct { 43 | sync.Mutex 44 | index map[uint16]*CPContext 45 | lastID uint16 46 | } 47 | 48 | // Request is the library provided MIDService's implementation of 49 | // the required interface function() 50 | func (m *MIDs) Request(c *CPContext) (uint16, error) { 51 | m.Lock() 52 | defer m.Unlock() 53 | 54 | for i, n := m.lastID, 0; n < 65535; i, n = i+1, n+1 { 55 | if i == 0 { 56 | i = 1 57 | } 58 | 59 | if _, ok := m.index[i]; !ok { 60 | m.index[i] = c 61 | m.lastID = i 62 | return i, nil 63 | } 64 | } 65 | 66 | return 0, ErrNoMoreIDs 67 | } 68 | 69 | // Get is the library provided MIDService's implementation of 70 | // the required interface function() 71 | func (m *MIDs) Get(i uint16) *CPContext { 72 | m.Lock() 73 | defer m.Unlock() 74 | return m.index[i] 75 | } 76 | 77 | // Free is the library provided MIDService's implementation of 78 | // the required interface function() 79 | func (m *MIDs) Free(i uint16) { 80 | m.Lock() 81 | delete(m.index, i) 82 | m.Unlock() 83 | } 84 | 85 | // Clear is the library provided MIDService's implementation of 86 | // the required interface function() 87 | func (m *MIDs) Clear() { 88 | m.index = make(map[uint16]*CPContext) 89 | } 90 | -------------------------------------------------------------------------------- /packets/unsuback.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Unsuback is the Variable Header definition for a Unsuback control packet 10 | type Unsuback struct { 11 | Reasons []byte 12 | Properties *Properties 13 | PacketID uint16 14 | } 15 | 16 | // UnsubackSuccess, etc are the list of valid unsuback reason codes. 17 | const ( 18 | UnsubackSuccess = 0x00 19 | UnsubackNoSubscriptionFound = 0x11 20 | UnsubackUnspecifiedError = 0x80 21 | UnsubackImplementationSpecificError = 0x83 22 | UnsubackNotAuthorized = 0x87 23 | UnsubackTopicFilterInvalid = 0x8F 24 | UnsubackPacketIdentifierInUse = 0x91 25 | ) 26 | 27 | // Unpack is the implementation of the interface required function for a packet 28 | func (u *Unsuback) Unpack(r *bytes.Buffer) error { 29 | var err error 30 | u.PacketID, err = readUint16(r) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | err = u.Properties.Unpack(r, UNSUBACK) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | u.Reasons = r.Bytes() 41 | 42 | return nil 43 | } 44 | 45 | // Buffers is the implementation of the interface required function for a packet 46 | func (u *Unsuback) Buffers() net.Buffers { 47 | var b bytes.Buffer 48 | writeUint16(u.PacketID, &b) 49 | idvp := u.Properties.Pack(UNSUBACK) 50 | propLen := encodeVBI(len(idvp)) 51 | return net.Buffers{b.Bytes(), propLen, idvp, u.Reasons} 52 | } 53 | 54 | // WriteTo is the implementation of the interface required function for a packet 55 | func (u *Unsuback) WriteTo(w io.Writer) (int64, error) { 56 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: UNSUBACK}} 57 | cp.Content = u 58 | 59 | return cp.WriteTo(w) 60 | } 61 | 62 | // Reason returns a string representation of the meaning of the ReasonCode 63 | func (u *Unsuback) Reason(index int) string { 64 | if index >= 0 && index < len(u.Reasons) { 65 | switch u.Reasons[index] { 66 | case 0x00: 67 | return "Success - The subscription is deleted" 68 | case 0x11: 69 | return "No subscription found - No matching Topic Filter is being used by the Client." 70 | case 0x80: 71 | return "Unspecified error - The unsubscribe could not be completed and the Server either does not wish to reveal the reason or none of the other Reason Codes apply." 72 | case 0x83: 73 | return "Implementation specific error - The UNSUBSCRIBE is valid but the Server does not accept it." 74 | case 0x87: 75 | return "Not authorized - The Client is not authorized to unsubscribe." 76 | case 0x8F: 77 | return "Topic Filter invalid - The Topic Filter is correctly formed but is not allowed for this Client." 78 | case 0x91: 79 | return "Packet Identifier in use - The specified Packet Identifier is already in use." 80 | } 81 | } 82 | return "Invalid Reason index" 83 | } 84 | -------------------------------------------------------------------------------- /paho/persistence.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/netdata/paho.golang/packets" 7 | ) 8 | 9 | // Persistence is an interface of the functions for a struct 10 | // that is used to persist ControlPackets. 11 | // Open() is an initialiser to prepare the Persistence for use 12 | // Put() takes a uint16 which is a messageid and a ControlPacket 13 | // to persist against that messageid 14 | // Get() takes a uint16 which is a messageid and returns the 15 | // persisted ControlPacket from the Persistence for that messageid 16 | // All() returns a slice of all ControlPackets persisted 17 | // Delete() takes a uint16 which is a messageid and deletes the 18 | // associated stored ControlPacket from the Persistence 19 | // Close() closes the Persistence 20 | // Reset() clears the Persistence and prepares it to be reused 21 | type Persistence interface { 22 | Open() 23 | Put(uint16, packets.ControlPacket) 24 | Get(uint16) packets.ControlPacket 25 | All() []packets.ControlPacket 26 | Delete(uint16) 27 | Close() 28 | Reset() 29 | } 30 | 31 | // MemoryPersistence is an implementation of a Persistence 32 | // that stores the ControlPackets in memory using a map 33 | type MemoryPersistence struct { 34 | sync.RWMutex 35 | packets map[uint16]packets.ControlPacket 36 | } 37 | 38 | // Open is the library provided MemoryPersistence's implementation of 39 | // the required interface function() 40 | func (m *MemoryPersistence) Open() { 41 | m.Lock() 42 | m.packets = make(map[uint16]packets.ControlPacket) 43 | m.Unlock() 44 | } 45 | 46 | // Put is the library provided MemoryPersistence's implementation of 47 | // the required interface function() 48 | func (m *MemoryPersistence) Put(id uint16, cp packets.ControlPacket) { 49 | m.Lock() 50 | m.packets[id] = cp 51 | m.Unlock() 52 | } 53 | 54 | // Get is the library provided MemoryPersistence's implementation of 55 | // the required interface function() 56 | func (m *MemoryPersistence) Get(id uint16) packets.ControlPacket { 57 | m.RLock() 58 | defer m.RUnlock() 59 | return m.packets[id] 60 | } 61 | 62 | // All is the library provided MemoryPersistence's implementation of 63 | // the required interface function() 64 | func (m *MemoryPersistence) All() []packets.ControlPacket { 65 | m.Lock() 66 | defer m.RUnlock() 67 | ret := make([]packets.ControlPacket, len(m.packets)) 68 | 69 | for _, cp := range m.packets { 70 | ret = append(ret, cp) 71 | } 72 | 73 | return ret 74 | } 75 | 76 | // Delete is the library provided MemoryPersistence's implementation of 77 | // the required interface function() 78 | func (m *MemoryPersistence) Delete(id uint16) { 79 | m.Lock() 80 | delete(m.packets, id) 81 | m.Unlock() 82 | } 83 | 84 | // Close is the library provided MemoryPersistence's implementation of 85 | // the required interface function() 86 | func (m *MemoryPersistence) Close() { 87 | m.Lock() 88 | m.packets = nil 89 | m.Unlock() 90 | } 91 | 92 | // Reset is the library provided MemoryPersistence's implementation of 93 | // the required interface function() 94 | func (m *MemoryPersistence) Reset() { 95 | m.Lock() 96 | m.packets = make(map[uint16]packets.ControlPacket) 97 | m.Unlock() 98 | } 99 | -------------------------------------------------------------------------------- /paho/cmd/chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "syscall" 14 | 15 | "github.com/netdata/paho.golang/paho" 16 | ) 17 | 18 | func main() { 19 | stdin := bufio.NewReader(os.Stdin) 20 | hostname, _ := os.Hostname() 21 | 22 | server := flag.String("server", "127.0.0.1:1883", "The full URL of the MQTT server to connect to") 23 | topic := flag.String("topic", hostname, "Topic to publish and receive the messages on") 24 | qos := flag.Int("qos", 0, "The QoS to send the messages at") 25 | //name := flag.String("chatname", hostname, "The name to attach to your messages") 26 | clientid := flag.String("clientid", "", "A clientid for the connection") 27 | username := flag.String("username", "", "A username to authenticate to the MQTT server") 28 | password := flag.String("password", "", "Password to match username") 29 | flag.Parse() 30 | 31 | conn, err := net.Dial("tcp", *server) 32 | if err != nil { 33 | log.Fatalf("Failed to connect to %s: %s", *server, err) 34 | } 35 | 36 | c := paho.NewClient(paho.ClientConfig{ 37 | Router: paho.NewSingleHandlerRouter(func(m *paho.Publish) { 38 | log.Printf("%s : %s", m.Properties.User["chatname"], string(m.Payload)) 39 | }), 40 | Conn: conn, 41 | }) 42 | 43 | cp := &paho.Connect{ 44 | KeepAlive: 30, 45 | ClientID: *clientid, 46 | CleanStart: true, 47 | Username: *username, 48 | Password: []byte(*password), 49 | } 50 | 51 | if *username != "" { 52 | cp.UsernameFlag = true 53 | } 54 | if *password != "" { 55 | cp.PasswordFlag = true 56 | } 57 | 58 | ca, err := c.Connect(context.Background(), cp) 59 | if err != nil { 60 | log.Fatalln(err) 61 | } 62 | if ca.ReasonCode != 0 { 63 | log.Fatalf("Failed to connect to %s : %d - %s", *server, ca.ReasonCode, ca.Properties.ReasonString) 64 | } 65 | 66 | fmt.Printf("Connected to %s\n", *server) 67 | 68 | ic := make(chan os.Signal, 1) 69 | signal.Notify(ic, os.Interrupt, syscall.SIGTERM) 70 | go func() { 71 | <-ic 72 | fmt.Println("signal received, exiting") 73 | if c != nil { 74 | d := &paho.Disconnect{ReasonCode: 0} 75 | err := c.Disconnect(d) 76 | if err != nil { 77 | log.Fatalf("failed to send Disconnect: %s", err) 78 | } 79 | } 80 | os.Exit(0) 81 | }() 82 | 83 | if _, err := c.Subscribe(context.Background(), &paho.Subscribe{ 84 | Subscriptions: map[string]paho.SubscribeOptions{ 85 | *topic: paho.SubscribeOptions{QoS: byte(*qos), NoLocal: true}, 86 | }, 87 | }); err != nil { 88 | log.Fatalln(err) 89 | } 90 | 91 | log.Printf("Subscribed to %s", *topic) 92 | 93 | for { 94 | message, err := stdin.ReadString('\n') 95 | if err == io.EOF { 96 | os.Exit(0) 97 | } 98 | 99 | pb := &paho.Publish{ 100 | Topic: *topic, 101 | QoS: byte(*qos), 102 | Payload: []byte(message), 103 | Properties: &paho.PublishProperties{ 104 | ResponseTopic: "my/response/topic", 105 | CorrelationData: []byte(""), 106 | ContentType: "application/json", 107 | }, 108 | } 109 | 110 | if _, err = c.Publish(context.Background(), pb); err != nil { 111 | log.Println(err) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packets/puback.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Puback is the Variable Header definition for a Puback control packet 10 | type Puback struct { 11 | Properties *Properties 12 | PacketID uint16 13 | ReasonCode byte 14 | } 15 | 16 | // PubackSuccess, etc are the list of valid puback reason codes. 17 | const ( 18 | PubackSuccess = 0x00 19 | PubackNoMatchingSubscribers = 0x10 20 | PubackUnspecifiedError = 0x80 21 | PubackImplementationSpecificError = 0x83 22 | PubackNotAuthorized = 0x87 23 | PubackTopicNameInvalid = 0x90 24 | PubackPacketIdentifierInUse = 0x91 25 | PubackQuotaExceeded = 0x97 26 | PubackPayloadFormatInvalid = 0x99 27 | ) 28 | 29 | //Unpack is the implementation of the interface required function for a packet 30 | func (p *Puback) Unpack(r *bytes.Buffer) error { 31 | var err error 32 | success := r.Len() == 2 33 | noProps := r.Len() == 3 34 | p.PacketID, err = readUint16(r) 35 | if err != nil { 36 | return err 37 | } 38 | if !success { 39 | p.ReasonCode, err = r.ReadByte() 40 | if err != nil { 41 | return err 42 | } 43 | 44 | if !noProps { 45 | err = p.Properties.Unpack(r, PUBACK) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | // Buffers is the implementation of the interface required function for a packet 55 | func (p *Puback) Buffers() net.Buffers { 56 | var b bytes.Buffer 57 | writeUint16(p.PacketID, &b) 58 | b.WriteByte(p.ReasonCode) 59 | idvp := p.Properties.Pack(PUBACK) 60 | propLen := encodeVBI(len(idvp)) 61 | n := net.Buffers{b.Bytes(), propLen} 62 | if len(idvp) > 0 { 63 | n = append(n, idvp) 64 | } 65 | return n 66 | } 67 | 68 | // WriteTo is the implementation of the interface required function for a packet 69 | func (p *Puback) WriteTo(w io.Writer) (int64, error) { 70 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBACK}} 71 | cp.Content = p 72 | 73 | return cp.WriteTo(w) 74 | } 75 | 76 | // Reason returns a string representation of the meaning of the ReasonCode 77 | func (p *Puback) Reason() string { 78 | switch p.ReasonCode { 79 | case 0: 80 | return "The message is accepted. Publication of the QoS 1 message proceeds." 81 | case 16: 82 | return "The message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success)." 83 | case 128: 84 | return "The receiver does not accept the publish but either does not want to reveal the reason, or it does not match one of the other values." 85 | case 131: 86 | return "The PUBLISH is valid but the receiver is not willing to accept it." 87 | case 135: 88 | return "The PUBLISH is not authorized." 89 | case 144: 90 | return "The Topic Name is not malformed, but is not accepted by this Client or Server." 91 | case 145: 92 | return "The Packet Identifier is already in use. This might indicate a mismatch in the Session State between the Client and Server." 93 | case 151: 94 | return "An implementation or administrative imposed limit has been exceeded." 95 | case 153: 96 | return "The payload format does not match the specified Payload Format Indicator." 97 | } 98 | 99 | return "" 100 | } 101 | -------------------------------------------------------------------------------- /paho/cp_publish.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/netdata/paho.golang/packets" 8 | ) 9 | 10 | type ( 11 | // Publish is a reporesentation of the MQTT Publish packet 12 | Publish struct { 13 | QoS byte 14 | Retain bool 15 | Topic string 16 | Properties *PublishProperties 17 | Payload []byte 18 | } 19 | 20 | // PublishProperties is a struct of the properties that can be set 21 | // for a Publish packet 22 | PublishProperties struct { 23 | CorrelationData []byte 24 | ContentType string 25 | ResponseTopic string 26 | PayloadFormat *byte 27 | MessageExpiry *uint32 28 | SubscriptionIdentifier *uint32 29 | TopicAlias *uint16 30 | User map[string]string 31 | } 32 | ) 33 | 34 | // InitProperties is a function that takes a lower level 35 | // Properties struct and completes the properties of the Publish on 36 | // which it is called 37 | func (p *Publish) InitProperties(prop *packets.Properties) { 38 | p.Properties = &PublishProperties{ 39 | PayloadFormat: prop.PayloadFormat, 40 | MessageExpiry: prop.MessageExpiry, 41 | ContentType: prop.ContentType, 42 | ResponseTopic: prop.ResponseTopic, 43 | CorrelationData: prop.CorrelationData, 44 | TopicAlias: prop.TopicAlias, 45 | SubscriptionIdentifier: prop.SubscriptionIdentifier, 46 | User: prop.User, 47 | } 48 | } 49 | 50 | // Packet returns a packets library Publish from the paho Publish 51 | // on which it is called 52 | func (p *Publish) Packet() *packets.Publish { 53 | v := &packets.Publish{ 54 | QoS: p.QoS, 55 | Retain: p.Retain, 56 | Topic: p.Topic, 57 | Payload: p.Payload, 58 | } 59 | if p.Properties != nil { 60 | v.Properties = &packets.Properties{ 61 | PayloadFormat: p.Properties.PayloadFormat, 62 | MessageExpiry: p.Properties.MessageExpiry, 63 | ContentType: p.Properties.ContentType, 64 | ResponseTopic: p.Properties.ResponseTopic, 65 | CorrelationData: p.Properties.CorrelationData, 66 | TopicAlias: p.Properties.TopicAlias, 67 | SubscriptionIdentifier: p.Properties.SubscriptionIdentifier, 68 | User: p.Properties.User, 69 | } 70 | } 71 | 72 | return v 73 | } 74 | 75 | func (p *Publish) String() string { 76 | var b bytes.Buffer 77 | 78 | fmt.Fprintf(&b, "topic: %s qos: %d retain: %t\n", p.Topic, p.QoS, p.Retain) 79 | if p.Properties.PayloadFormat != nil { 80 | fmt.Fprintf(&b, "PayloadFormat: %v\n", p.Properties.PayloadFormat) 81 | } 82 | if p.Properties.MessageExpiry != nil { 83 | fmt.Fprintf(&b, "MessageExpiry: %v\n", p.Properties.MessageExpiry) 84 | } 85 | if p.Properties.ContentType != "" { 86 | fmt.Fprintf(&b, "ContentType: %v\n", p.Properties.ContentType) 87 | } 88 | if p.Properties.ResponseTopic != "" { 89 | fmt.Fprintf(&b, "ResponseTopic: %v\n", p.Properties.ResponseTopic) 90 | } 91 | if p.Properties.CorrelationData != nil { 92 | fmt.Fprintf(&b, "CorrelationData: %v\n", p.Properties.CorrelationData) 93 | } 94 | if p.Properties.SubscriptionIdentifier != nil { 95 | fmt.Fprintf(&b, "SubscriptionIdentifier: %v\n", p.Properties.SubscriptionIdentifier) 96 | } 97 | for k, v := range p.Properties.User { 98 | fmt.Fprintf(&b, "User: %s : %s\n", k, v) 99 | } 100 | b.WriteString(string(p.Payload)) 101 | 102 | return b.String() 103 | } 104 | -------------------------------------------------------------------------------- /packets/pubrec.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Pubrec is the Variable Header definition for a Pubrec control packet 10 | type Pubrec struct { 11 | Properties *Properties 12 | PacketID uint16 13 | ReasonCode byte 14 | } 15 | 16 | // PubrecSuccess, etc are the list of valid Pubrec reason codes 17 | const ( 18 | PubrecSuccess = 0x00 19 | PubrecNoMatchingSubscribers = 0x10 20 | PubrecUnspecifiedError = 0x80 21 | PubrecImplementationSpecificError = 0x83 22 | PubrecNotAuthorized = 0x87 23 | PubrecTopicNameInvalid = 0x90 24 | PubrecPacketIdentifierInUse = 0x91 25 | PubrecQuotaExceeded = 0x97 26 | PubrecPayloadFormatInvalid = 0x99 27 | ) 28 | 29 | //Unpack is the implementation of the interface required function for a packet 30 | func (p *Pubrec) Unpack(r *bytes.Buffer) error { 31 | var err error 32 | success := r.Len() == 2 33 | noProps := r.Len() == 3 34 | p.PacketID, err = readUint16(r) 35 | if err != nil { 36 | return err 37 | } 38 | if !success { 39 | p.ReasonCode, err = r.ReadByte() 40 | if err != nil { 41 | return err 42 | } 43 | 44 | if !noProps { 45 | err = p.Properties.Unpack(r, PUBACK) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // Buffers is the implementation of the interface required function for a packet 56 | func (p *Pubrec) Buffers() net.Buffers { 57 | var b bytes.Buffer 58 | writeUint16(p.PacketID, &b) 59 | b.WriteByte(p.ReasonCode) 60 | n := net.Buffers{b.Bytes()} 61 | idvp := p.Properties.Pack(PUBREC) 62 | propLen := encodeVBI(len(idvp)) 63 | if len(idvp) > 0 { 64 | n = append(n, propLen) 65 | n = append(n, idvp) 66 | } 67 | return n 68 | } 69 | 70 | // WriteTo is the implementation of the interface required function for a packet 71 | func (p *Pubrec) WriteTo(w io.Writer) (int64, error) { 72 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBREC}} 73 | cp.Content = p 74 | 75 | return cp.WriteTo(w) 76 | } 77 | 78 | // Reason returns a string representation of the meaning of the ReasonCode 79 | func (p *Pubrec) Reason() string { 80 | switch p.ReasonCode { 81 | case 0: 82 | return "Success - The message is accepted. Publication of the QoS 2 message proceeds." 83 | case 16: 84 | return "No matching subscribers. - The message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that case there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success)" 85 | case 128: 86 | return "Unspecified error - The receiver does not accept the publish but either does not want to reveal the reason, or it does not match one of the other values." 87 | case 131: 88 | return "Implementation specific error - The PUBLISH is valid but the receiver is not willing to accept it." 89 | case 135: 90 | return "Not authorized - The PUBLISH is not authorized." 91 | case 144: 92 | return "Topic Name invalid - The Topic Name is not malformed, but is not accepted by this Client or Server." 93 | case 145: 94 | return "Packet Identifier in use - The Packet Identifier is already in use. This might indicate a mismatch in the Session State between the Client and Server." 95 | case 151: 96 | return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." 97 | case 153: 98 | return "Payload format invalid - The payload format does not match the one specified in the Payload Format Indicator." 99 | } 100 | 101 | return "" 102 | } 103 | -------------------------------------------------------------------------------- /paho/trace.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/netdata/paho.golang/packets" 7 | ) 8 | 9 | type Trace struct { 10 | OnSend func(context.Context, *SendStartTrace) 11 | OnRecv func(context.Context, *RecvStartTrace) 12 | OnPublish func(context.Context, *PublishStartTrace) 13 | } 14 | 15 | type PublishStartTrace struct { 16 | Packet *packets.Publish 17 | OnDone func(context.Context, PublishDoneTrace) 18 | } 19 | 20 | func (p *PublishStartTrace) done(ctx context.Context, err error) { 21 | if p == nil || p.OnDone == nil { 22 | return 23 | } 24 | p.OnDone(ctx, PublishDoneTrace{ 25 | Error: err, 26 | }) 27 | } 28 | 29 | type PublishDoneTrace struct { 30 | Error error 31 | } 32 | 33 | func (c *Client) tracePublish(ctx context.Context, p *packets.Publish) *PublishStartTrace { 34 | fn := c.Trace.OnPublish 35 | if fn == nil { 36 | return nil 37 | } 38 | t := PublishStartTrace{ 39 | Packet: p, 40 | } 41 | fn(ctx, &t) 42 | return &t 43 | } 44 | 45 | type RecvStartTrace struct { 46 | OnDone func(context.Context, RecvDoneTrace) 47 | } 48 | 49 | type RecvDoneTrace struct { 50 | Packet interface{} 51 | PacketType packets.PacketType 52 | Error error 53 | } 54 | 55 | func (c *Client) traceRecv(ctx context.Context) *RecvStartTrace { 56 | fn := c.Trace.OnRecv 57 | if fn == nil { 58 | return nil 59 | } 60 | var t RecvStartTrace 61 | fn(ctx, &t) 62 | return &t 63 | } 64 | 65 | func (t *RecvStartTrace) done(ctx context.Context, x interface{}, err error) { 66 | if t == nil || t.OnDone == nil { 67 | return 68 | } 69 | t.OnDone(ctx, RecvDoneTrace{ 70 | Packet: x, 71 | PacketType: matchPacketType(x), 72 | Error: err, 73 | }) 74 | } 75 | 76 | type SendStartTrace struct { 77 | Packet interface{} 78 | PacketType packets.PacketType 79 | 80 | OnDone func(context.Context, SendDoneTrace) 81 | } 82 | 83 | type SendDoneTrace struct { 84 | Error error 85 | } 86 | 87 | func (c *Client) traceSend(ctx context.Context, x interface{}) *SendStartTrace { 88 | fn := c.Trace.OnSend 89 | if fn == nil { 90 | return nil 91 | } 92 | t := SendStartTrace{ 93 | Packet: x, 94 | PacketType: matchPacketType(x), 95 | } 96 | fn(ctx, &t) 97 | return &t 98 | } 99 | 100 | func (t *SendStartTrace) done(ctx context.Context, err error) { 101 | if t != nil && t.OnDone != nil { 102 | t.OnDone(ctx, SendDoneTrace{ 103 | Error: err, 104 | }) 105 | } 106 | } 107 | 108 | func matchPacketType(x interface{}) packets.PacketType { 109 | if x == nil { 110 | return 0 111 | } 112 | switch p := x.(type) { 113 | case *packets.ControlPacket: 114 | if p == nil { 115 | return 0 116 | } 117 | return p.FixedHeader.Type 118 | 119 | case *packets.Connect: 120 | return packets.CONNECT 121 | case *packets.Connack: 122 | return packets.CONNACK 123 | case *packets.Publish: 124 | return packets.PUBLISH 125 | case *packets.Puback: 126 | return packets.PUBACK 127 | case *packets.Pubrec: 128 | return packets.PUBREC 129 | case *packets.Pubrel: 130 | return packets.PUBREL 131 | case *packets.Pubcomp: 132 | return packets.PUBCOMP 133 | case *packets.Subscribe: 134 | return packets.SUBSCRIBE 135 | case *packets.Suback: 136 | return packets.SUBACK 137 | case *packets.Unsubscribe: 138 | return packets.UNSUBSCRIBE 139 | case *packets.Unsuback: 140 | return packets.UNSUBACK 141 | case *packets.Pingreq: 142 | return packets.PINGREQ 143 | case *packets.Pingresp: 144 | return packets.PINGRESP 145 | case *packets.Disconnect: 146 | return packets.DISCONNECT 147 | case *packets.Auth: 148 | return packets.AUTH 149 | 150 | default: 151 | return 0 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /packets/suback.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Suback is the Variable Header definition for a Suback control packet 10 | type Suback struct { 11 | Properties *Properties 12 | Reasons []byte 13 | PacketID uint16 14 | } 15 | 16 | // SubackGrantedQoS0, etc are the list of valid suback reason codes. 17 | const ( 18 | SubackGrantedQoS0 = 0x00 19 | SubackGrantedQoS1 = 0x01 20 | SubackGrantedQoS2 = 0x02 21 | SubackUnspecifiederror = 0x80 22 | SubackImplementationspecificerror = 0x83 23 | SubackNotauthorized = 0x87 24 | SubackTopicFilterinvalid = 0x8F 25 | SubackPacketIdentifierinuse = 0x91 26 | SubackQuotaexceeded = 0x97 27 | SubackSharedSubscriptionnotsupported = 0x9E 28 | SubackSubscriptionIdentifiersnotsupported = 0xA1 29 | SubackWildcardsubscriptionsnotsupported = 0xA2 30 | ) 31 | 32 | //Unpack is the implementation of the interface required function for a packet 33 | func (s *Suback) Unpack(r *bytes.Buffer) error { 34 | var err error 35 | s.PacketID, err = readUint16(r) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | err = s.Properties.Unpack(r, SUBACK) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | s.Reasons = r.Bytes() 46 | 47 | return nil 48 | } 49 | 50 | // Buffers is the implementation of the interface required function for a packet 51 | func (s *Suback) Buffers() net.Buffers { 52 | var b bytes.Buffer 53 | writeUint16(s.PacketID, &b) 54 | idvp := s.Properties.Pack(SUBACK) 55 | propLen := encodeVBI(len(idvp)) 56 | return net.Buffers{b.Bytes(), propLen, idvp, s.Reasons} 57 | } 58 | 59 | // WriteTo is the implementation of the interface required function for a packet 60 | func (s *Suback) WriteTo(w io.Writer) (int64, error) { 61 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: SUBACK}} 62 | cp.Content = s 63 | 64 | return cp.WriteTo(w) 65 | } 66 | 67 | // Reason returns a string representation of the meaning of the ReasonCode 68 | func (s *Suback) Reason(index int) string { 69 | if index >= 0 && index < len(s.Reasons) { 70 | switch s.Reasons[index] { 71 | case 0: 72 | return "Granted QoS 0 - The subscription is accepted and the maximum QoS sent will be QoS 0. This might be a lower QoS than was requested." 73 | case 1: 74 | return "Granted QoS 1 - The subscription is accepted and the maximum QoS sent will be QoS 1. This might be a lower QoS than was requested." 75 | case 2: 76 | return "Granted QoS 2 - The subscription is accepted and any received QoS will be sent to this subscription." 77 | case 128: 78 | return "Unspecified error - The subscription is not accepted and the Server either does not wish to reveal the reason or none of the other Reason Codes apply." 79 | case 131: 80 | return "Implementation specific error - The SUBSCRIBE is valid but the Server does not accept it." 81 | case 135: 82 | return "Not authorized - The Client is not authorized to make this subscription." 83 | case 143: 84 | return "Topic Filter invalid - The Topic Filter is correctly formed but is not allowed for this Client." 85 | case 145: 86 | return "Packet Identifier in use - The specified Packet Identifier is already in use." 87 | case 151: 88 | return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." 89 | case 158: 90 | return "Shared Subscription not supported - The Server does not support Shared Subscriptions for this Client." 91 | case 161: 92 | return "Subscription Identifiers not supported - The Server does not support Subscription Identifiers; the subscription is not accepted." 93 | case 162: 94 | return "Wildcard subscriptions not supported - The Server does not support Wildcard subscription; the subscription is not accepted." 95 | } 96 | } 97 | return "Invalid Reason index" 98 | } 99 | -------------------------------------------------------------------------------- /packets/connack.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Connack is the Variable Header definition for a connack control packet 10 | type Connack struct { 11 | Properties *Properties 12 | ReasonCode byte 13 | SessionPresent bool 14 | } 15 | 16 | //Unpack is the implementation of the interface required function for a packet 17 | func (c *Connack) Unpack(r *bytes.Buffer) error { 18 | connackFlags, err := r.ReadByte() 19 | if err != nil { 20 | return err 21 | } 22 | c.SessionPresent = connackFlags&0x01 > 0 23 | 24 | c.ReasonCode, err = r.ReadByte() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | err = c.Properties.Unpack(r, CONNACK) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // Buffers is the implementation of the interface required function for a packet 38 | func (c *Connack) Buffers() net.Buffers { 39 | var header bytes.Buffer 40 | 41 | if c.SessionPresent { 42 | header.WriteByte(1) 43 | } else { 44 | header.WriteByte(0) 45 | } 46 | header.WriteByte(c.ReasonCode) 47 | 48 | idvp := c.Properties.Pack(CONNACK) 49 | propLen := encodeVBI(len(idvp)) 50 | 51 | n := net.Buffers{header.Bytes(), propLen} 52 | if len(idvp) > 0 { 53 | n = append(n, idvp) 54 | } 55 | 56 | return n 57 | } 58 | 59 | // WriteTo is the implementation of the interface required function for a packet 60 | func (c *Connack) WriteTo(w io.Writer) (int64, error) { 61 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: CONNACK}} 62 | cp.Content = c 63 | 64 | return cp.WriteTo(w) 65 | } 66 | 67 | // Reason returns a string representation of the meaning of the ReasonCode 68 | func (c *Connack) Reason() string { 69 | switch c.ReasonCode { 70 | case 0: 71 | return "Success - The Connection is accepted." 72 | case 128: 73 | return "Unspecified error - The Server does not wish to reveal the reason for the failure, or none of the other Reason Codes apply." 74 | case 129: 75 | return "Malformed Packet - Data within the CONNECT packet could not be correctly parsed." 76 | case 130: 77 | return "Protocol Error - Data in the CONNECT packet does not conform to this specification." 78 | case 131: 79 | return "Implementation specific error - The CONNECT is valid but is not accepted by this Server." 80 | case 132: 81 | return "Unsupported Protocol Version - The Server does not support the version of the MQTT protocol requested by the Client." 82 | case 133: 83 | return "Client Identifier not valid - The Client Identifier is a valid string but is not allowed by the Server." 84 | case 134: 85 | return "Bad User Name or Password - The Server does not accept the User Name or Password specified by the Client" 86 | case 135: 87 | return "Not authorized - The Client is not authorized to connect." 88 | case 136: 89 | return "Server unavailable - The MQTT Server is not available." 90 | case 137: 91 | return "Server busy - The Server is busy. Try again later." 92 | case 138: 93 | return "Banned - This Client has been banned by administrative action. Contact the server administrator." 94 | case 140: 95 | return "Bad authentication method - The authentication method is not supported or does not match the authentication method currently in use." 96 | case 144: 97 | return "Topic Name invalid - The Will Topic Name is not malformed, but is not accepted by this Server." 98 | case 149: 99 | return "Packet too large - The CONNECT packet exceeded the maximum permissible size." 100 | case 151: 101 | return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." 102 | case 154: 103 | return "Retain not supported - The Server does not support retained messages, and Will Retain was set to 1." 104 | case 155: 105 | return "QoS not supported - The Server does not support the QoS set in Will QoS." 106 | case 156: 107 | return "Use another server - The Client should temporarily use another server." 108 | case 157: 109 | return "Server moved - The Client should permanently use another server." 110 | case 159: 111 | return "Connection rate exceeded - The connection rate limit has been exceeded." 112 | } 113 | 114 | return "" 115 | } 116 | -------------------------------------------------------------------------------- /packets/connect.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Connect is the Variable Header definition for a connect control packet 10 | type Connect struct { 11 | WillMessage []byte 12 | Password []byte 13 | Username string 14 | ProtocolName string 15 | ClientID string 16 | WillTopic string 17 | Properties *Properties 18 | WillProperties *Properties 19 | KeepAlive uint16 20 | ProtocolVersion byte 21 | WillQOS byte 22 | PasswordFlag bool 23 | UsernameFlag bool 24 | WillRetain bool 25 | WillFlag bool 26 | CleanStart bool 27 | } 28 | 29 | // PackFlags takes the Connect flags and packs them into the single byte 30 | // representation used on the wire by MQTT 31 | func (c *Connect) PackFlags() (f byte) { 32 | if c.UsernameFlag { 33 | f |= 0x01 << 7 34 | } 35 | if c.PasswordFlag { 36 | f |= 0x01 << 6 37 | } 38 | if c.WillFlag { 39 | f |= 0x01 << 2 40 | f |= c.WillQOS << 3 41 | if c.WillRetain { 42 | f |= 0x01 << 5 43 | } 44 | } 45 | if c.CleanStart { 46 | f |= 0x01 << 1 47 | } 48 | return 49 | } 50 | 51 | // UnpackFlags takes the wire byte representing the connect options flags 52 | // and fills out the appropriate variables in the struct 53 | func (c *Connect) UnpackFlags(b byte) { 54 | c.CleanStart = 1&(b>>1) > 0 55 | c.WillFlag = 1&(b>>2) > 0 56 | c.WillQOS = 3 & (b >> 3) 57 | c.WillRetain = 1&(b>>5) > 0 58 | c.PasswordFlag = 1&(b>>6) > 0 59 | c.UsernameFlag = 1&(b>>7) > 0 60 | } 61 | 62 | //Unpack is the implementation of the interface required function for a packet 63 | func (c *Connect) Unpack(r *bytes.Buffer) error { 64 | var err error 65 | 66 | if c.ProtocolName, err = readString(r); err != nil { 67 | return err 68 | } 69 | 70 | if c.ProtocolVersion, err = r.ReadByte(); err != nil { 71 | return err 72 | } 73 | 74 | flags, err := r.ReadByte() 75 | if err != nil { 76 | return err 77 | } 78 | c.UnpackFlags(flags) 79 | 80 | if c.KeepAlive, err = readUint16(r); err != nil { 81 | return err 82 | } 83 | 84 | err = c.Properties.Unpack(r, CONNECT) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | c.ClientID, err = readString(r) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if c.WillFlag { 95 | c.WillProperties = &Properties{User: make(map[string]string)} 96 | err = c.WillProperties.Unpack(r, CONNECT) 97 | if err != nil { 98 | return err 99 | } 100 | c.WillTopic, err = readString(r) 101 | if err != nil { 102 | return err 103 | } 104 | c.WillMessage, err = readBinary(r) 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | 110 | if c.UsernameFlag { 111 | c.Username, err = readString(r) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | 117 | if c.PasswordFlag { 118 | c.Password, err = readBinary(r) 119 | if err != nil { 120 | return err 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | // Buffers is the implementation of the interface required function for a packet 128 | func (c *Connect) Buffers() net.Buffers { 129 | var header bytes.Buffer 130 | var body bytes.Buffer 131 | writeString(c.ProtocolName, &header) 132 | header.WriteByte(c.ProtocolVersion) 133 | header.WriteByte(c.PackFlags()) 134 | writeUint16(c.KeepAlive, &header) 135 | idvp := c.Properties.Pack(CONNECT) 136 | propLen := encodeVBI(len(idvp)) 137 | 138 | writeString(c.ClientID, &body) 139 | if c.WillFlag { 140 | willIdvp := c.WillProperties.Pack(CONNECT) 141 | body.Write(encodeVBI(len(willIdvp))) 142 | body.Write(willIdvp) 143 | writeString(c.WillTopic, &body) 144 | writeBinary(c.WillMessage, &body) 145 | } 146 | if c.UsernameFlag { 147 | writeString(c.Username, &body) 148 | } 149 | if c.PasswordFlag { 150 | writeBinary(c.Password, &body) 151 | } 152 | 153 | return net.Buffers{header.Bytes(), propLen, idvp, body.Bytes()} 154 | } 155 | 156 | // WriteTo is the implementation of the interface required function for a packet 157 | func (c *Connect) WriteTo(w io.Writer) (int64, error) { 158 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: CONNECT}} 159 | cp.Content = c 160 | 161 | return cp.WriteTo(w) 162 | } 163 | -------------------------------------------------------------------------------- /packets/properties_test.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestPropertiess(t *testing.T) { 9 | if !ValidateID(PUBLISH, PropPayloadFormat) { 10 | t.Fatalf("'payloadFormat' is valid for 'PUBLISH' packets") 11 | } 12 | 13 | if !ValidateID(PUBLISH, PropMessageExpiry) { 14 | t.Fatalf("'messageExpiry' is valid for 'PUBLISH' packets") 15 | } 16 | 17 | if !ValidateID(PUBLISH, PropResponseTopic) { 18 | t.Fatalf("'responseTopic' is valid for 'PUBLISH' packets") 19 | } 20 | 21 | if !ValidateID(PUBLISH, PropCorrelationData) { 22 | t.Fatalf("'correlationData' is valid for 'PUBLISH' packets") 23 | } 24 | 25 | if !ValidateID(CONNECT, PropSessionExpiryInterval) { 26 | t.Fatalf("'sessionExpiryInterval' is valid for 'CONNECT' packets") 27 | } 28 | 29 | if !ValidateID(DISCONNECT, PropSessionExpiryInterval) { 30 | t.Fatalf("'sessionExpiryInterval' is valid for 'DISCONNECT' packets") 31 | } 32 | 33 | if !ValidateID(CONNACK, PropAssignedClientID) { 34 | t.Fatalf("'assignedClientID' is valid for 'CONNACK' packets") 35 | } 36 | 37 | if !ValidateID(CONNACK, PropServerKeepAlive) { 38 | t.Fatalf("'serverKeepAlive' is valid for 'CONNACK' packets") 39 | } 40 | 41 | if !ValidateID(CONNECT, PropAuthMethod) { 42 | t.Fatalf("'authMethod' is valid for 'CONNECT' packets") 43 | } 44 | 45 | if !ValidateID(CONNACK, PropAuthMethod) { 46 | t.Fatalf("'authMethod' is valid for 'CONNACK' packets") 47 | } 48 | 49 | if !ValidateID(AUTH, PropAuthMethod) { 50 | t.Fatalf("'authMethod' is valid for 'auth' packets") 51 | } 52 | 53 | if !ValidateID(CONNECT, PropAuthData) { 54 | t.Fatalf("'authData' is valid for 'CONNECT' packets") 55 | } 56 | 57 | if !ValidateID(CONNACK, PropAuthData) { 58 | t.Fatalf("'authData' is valid for 'CONNACK' packets") 59 | } 60 | 61 | if !ValidateID(AUTH, PropAuthData) { 62 | t.Fatalf("'authData' is valid for 'auth' packets") 63 | } 64 | 65 | if !ValidateID(CONNECT, PropRequestProblemInfo) { 66 | t.Fatalf("'requestProblemInfo' is valid for 'CONNECT' packets") 67 | } 68 | 69 | if !ValidateID(CONNECT, PropWillDelayInterval) { 70 | t.Fatalf("'willDelayInterval' is valid for 'CONNECT' packets") 71 | } 72 | 73 | if !ValidateID(CONNECT, PropRequestResponseInfo) { 74 | t.Fatalf("'requestResponseInfo' is valid for 'CONNECT' packets") 75 | } 76 | 77 | if !ValidateID(CONNACK, PropResponseInfo) { 78 | t.Fatalf("'ResponseInfo' is valid for 'CONNACK' packets") 79 | } 80 | 81 | if !ValidateID(CONNACK, PropServerReference) { 82 | t.Fatalf("'serverReference' is valid for 'CONNACK' packets") 83 | } 84 | 85 | if !ValidateID(DISCONNECT, PropServerReference) { 86 | t.Fatalf("'serverReference' is valid for 'DISCONNECT' packets") 87 | } 88 | 89 | if !ValidateID(CONNACK, PropReasonString) { 90 | t.Fatalf("'reasonString' is valid for 'CONNACK' packets") 91 | } 92 | 93 | if !ValidateID(DISCONNECT, PropReasonString) { 94 | t.Fatalf("'reasonString' is valid for 'DISCONNECT' packets") 95 | } 96 | 97 | if !ValidateID(CONNECT, PropReceiveMaximum) { 98 | t.Fatalf("'receiveMaximum' is valid for 'CONNECT' packets") 99 | } 100 | 101 | if !ValidateID(CONNACK, PropReceiveMaximum) { 102 | t.Fatalf("'receiveMaximum' is valid for 'CONNACK' packets") 103 | } 104 | 105 | if !ValidateID(CONNECT, PropTopicAliasMaximum) { 106 | t.Fatalf("'topicAliasMaximum' is valid for 'CONNECT' packets") 107 | } 108 | 109 | if !ValidateID(CONNACK, PropTopicAliasMaximum) { 110 | t.Fatalf("'topicAliasMaximum' is valid for 'CONNACK' packets") 111 | } 112 | 113 | if !ValidateID(PUBLISH, PropTopicAlias) { 114 | t.Fatalf("'topicAlias' is valid for 'PUBLISH' packets") 115 | } 116 | 117 | if !ValidateID(CONNECT, PropMaximumQOS) { 118 | t.Fatalf("'maximumQOS' is valid for 'CONNECT' packets") 119 | } 120 | 121 | if !ValidateID(CONNACK, PropMaximumQOS) { 122 | t.Fatalf("'maximumQOS' is valid for 'CONNACK' packets") 123 | } 124 | 125 | if !ValidateID(CONNACK, PropRetainAvailable) { 126 | t.Fatalf("'retainAvailable' is valid for 'CONNACK' packets") 127 | } 128 | 129 | if !ValidateID(CONNECT, PropUser) { 130 | t.Fatalf("'user' is valid for 'CONNECT' packets") 131 | } 132 | 133 | if !ValidateID(PUBLISH, PropUser) { 134 | t.Fatalf("'user' is valid for 'PUBLISH' packets") 135 | } 136 | } 137 | 138 | func TestInvalidPropertiess(t *testing.T) { 139 | if ValidateID(PUBLISH, PropRequestResponseInfo) { 140 | t.Fatalf("'requestReplyInfo' is invalid for 'PUBLISH' packets") 141 | } 142 | } 143 | 144 | func BenchmarkPropertyCreationStruct(b *testing.B) { 145 | var p *Properties 146 | pf := byte(1) 147 | pe := uint32(32) 148 | for i := 0; i < b.N; i++ { 149 | p = &Properties{ 150 | PayloadFormat: &pf, 151 | MessageExpiry: &pe, 152 | ContentType: "mime/json", 153 | ResponseTopic: "x/y", 154 | CorrelationData: []byte("corelid"), 155 | } 156 | } 157 | fmt.Sprintln(p) 158 | } 159 | -------------------------------------------------------------------------------- /paho/server_test.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "log" 5 | "net" 6 | 7 | "github.com/netdata/paho.golang/packets" 8 | ) 9 | 10 | type fakeAuth struct{} 11 | 12 | func (f *fakeAuth) Authenticate(a *Auth) *Auth { 13 | return &Auth{ 14 | Properties: &AuthProperties{ 15 | AuthMethod: "TEST", 16 | AuthData: []byte("secret data"), 17 | }, 18 | } 19 | } 20 | 21 | func (f *fakeAuth) Authenticated() {} 22 | 23 | type testServer struct { 24 | conn net.Conn 25 | clientConn net.Conn 26 | stop chan struct{} 27 | responses map[packets.PacketType]packets.Packet 28 | } 29 | 30 | func newTestServer() *testServer { 31 | t := &testServer{ 32 | stop: make(chan struct{}), 33 | responses: make(map[packets.PacketType]packets.Packet), 34 | } 35 | t.conn, t.clientConn = net.Pipe() 36 | 37 | return t 38 | } 39 | 40 | func (t *testServer) ClientConn() net.Conn { 41 | return t.clientConn 42 | } 43 | 44 | func (t *testServer) SetResponse(pt packets.PacketType, p packets.Packet) { 45 | t.responses[pt] = p 46 | } 47 | 48 | func (t *testServer) SendPacket(p packets.Packet) error { 49 | _, err := p.WriteTo(t.conn) 50 | 51 | return err 52 | } 53 | 54 | func (t *testServer) Stop() { 55 | t.conn.Close() 56 | close(t.stop) 57 | } 58 | 59 | func (t *testServer) Run() { 60 | for { 61 | select { 62 | case <-t.stop: 63 | return 64 | default: 65 | recv, err := packets.ReadPacket(t.conn) 66 | if err != nil { 67 | log.Println("error in test server reading packet", err) 68 | return 69 | } 70 | log.Println("test server received a control packet:", recv.Type) 71 | switch recv.Type { 72 | case packets.CONNECT: 73 | log.Println("received connect", recv.Content.(*packets.Connect)) 74 | if p, ok := t.responses[packets.CONNACK]; ok { 75 | if _, err := p.WriteTo(t.conn); err != nil { 76 | log.Println(err) 77 | } 78 | } else { 79 | p := packets.NewControlPacket(packets.CONNACK) 80 | if _, err := p.WriteTo(t.conn); err != nil { 81 | log.Println(err) 82 | } 83 | } 84 | case packets.SUBSCRIBE: 85 | log.Println("received subscribe", recv.Content.(*packets.Subscribe)) 86 | if p, ok := t.responses[packets.SUBACK]; ok { 87 | p.(*packets.Suback).PacketID = recv.PacketID() 88 | if _, err := p.WriteTo(t.conn); err != nil { 89 | log.Println(err) 90 | } 91 | } 92 | case packets.UNSUBSCRIBE: 93 | log.Println("received unsubscribe", recv.Content.(*packets.Unsubscribe)) 94 | if p, ok := t.responses[packets.UNSUBACK]; ok { 95 | p.(*packets.Unsuback).PacketID = recv.PacketID() 96 | if _, err := p.WriteTo(t.conn); err != nil { 97 | log.Println(err) 98 | } 99 | } 100 | case packets.AUTH: 101 | log.Println("received auth", recv.Content.(*packets.Auth)) 102 | if p, ok := t.responses[packets.AUTH]; ok { 103 | log.Println("sending auth") 104 | if _, err := p.WriteTo(t.conn); err != nil { 105 | log.Println(err) 106 | } 107 | } 108 | case packets.PUBLISH: 109 | log.Println("received publish", recv.Content.(*packets.Publish)) 110 | switch recv.Content.(*packets.Publish).QoS { 111 | case 1: 112 | if p, ok := t.responses[packets.PUBACK]; ok { 113 | p.(*packets.Puback).PacketID = recv.PacketID() 114 | if _, err := p.WriteTo(t.conn); err != nil { 115 | log.Println(err) 116 | } 117 | } 118 | case 2: 119 | if p, ok := t.responses[packets.PUBREC]; ok { 120 | p.(*packets.Pubrec).PacketID = recv.PacketID() 121 | log.Println("sending pubrec") 122 | if _, err := p.WriteTo(t.conn); err != nil { 123 | log.Println(err) 124 | } 125 | log.Println("sent pubrec") 126 | } 127 | } 128 | 129 | case packets.PUBACK: 130 | log.Println("recevied puback", recv.Content.(*packets.Puback)) 131 | case packets.PUBCOMP: 132 | log.Println("received pubcomp", recv.Content.(*packets.Pubcomp)) 133 | case packets.SUBACK: 134 | log.Println("received suback") 135 | case packets.UNSUBACK: 136 | log.Println("received unsuback") 137 | case packets.PUBREC: 138 | log.Println("received pubrec", recv.Content.(*packets.Pubrec)) 139 | if p, ok := t.responses[packets.PUBREL]; ok { 140 | p.(*packets.Pubrel).PacketID = recv.PacketID() 141 | if _, err := p.WriteTo(t.conn); err != nil { 142 | log.Println(err) 143 | } 144 | } 145 | case packets.PUBREL: 146 | log.Println("received pubrel", recv.Content.(*packets.Pubrel)) 147 | if p, ok := t.responses[packets.PUBCOMP]; ok { 148 | p.(*packets.Pubcomp).PacketID = recv.PacketID() 149 | if _, err := p.WriteTo(t.conn); err != nil { 150 | log.Println(err) 151 | } 152 | } 153 | case packets.DISCONNECT: 154 | log.Println("recevied disconnect") 155 | case packets.PINGREQ: 156 | log.Println("test server sending pingresp") 157 | pr := packets.NewControlPacket(packets.PINGRESP) 158 | pr.WriteTo(t.conn) 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /paho/cmd/rpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "sync" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/netdata/paho.golang/paho" 18 | "github.com/netdata/paho.golang/paho/extensions/rpc" 19 | ) 20 | 21 | func init() { 22 | ic := make(chan os.Signal, 1) 23 | signal.Notify(ic, os.Interrupt, syscall.SIGTERM) 24 | go func() { 25 | <-ic 26 | os.Exit(0) 27 | }() 28 | } 29 | 30 | type Request struct { 31 | Function string `json:"function"` 32 | Param1 int `json:"param1"` 33 | Param2 int `json:"param2"` 34 | } 35 | 36 | type Response struct { 37 | Value int `json:"value"` 38 | } 39 | 40 | func listener(server, rTopic, username, password string) { 41 | var v sync.WaitGroup 42 | 43 | v.Add(1) 44 | 45 | go func() { 46 | conn, err := net.Dial("tcp", server) 47 | if err != nil { 48 | log.Fatalf("Failed to connect to %s: %s", server, err) 49 | } 50 | 51 | c := paho.NewClient(paho.ClientConfig{ 52 | Conn: conn, 53 | }) 54 | c.Router = paho.NewSingleHandlerRouter(func(m *paho.Publish) { 55 | if m.Properties != nil && m.Properties.CorrelationData != nil && m.Properties.ResponseTopic != "" { 56 | log.Printf("Received message with response topic %s and correl id %s\n%s", m.Properties.ResponseTopic, string(m.Properties.CorrelationData), string(m.Payload)) 57 | 58 | var r Request 59 | var resp Response 60 | 61 | if err := json.NewDecoder(bytes.NewReader(m.Payload)).Decode(&r); err != nil { 62 | log.Printf("Failed to decode Request: %v", err) 63 | } 64 | 65 | switch r.Function { 66 | case "add": 67 | resp.Value = r.Param1 + r.Param2 68 | case "mul": 69 | resp.Value = r.Param1 * r.Param2 70 | case "div": 71 | resp.Value = r.Param1 / r.Param2 72 | case "sub": 73 | resp.Value = r.Param1 - r.Param2 74 | } 75 | 76 | body, _ := json.Marshal(resp) 77 | _, err := c.Publish(context.Background(), &paho.Publish{ 78 | Properties: &paho.PublishProperties{ 79 | CorrelationData: m.Properties.CorrelationData, 80 | }, 81 | Topic: m.Properties.ResponseTopic, 82 | Payload: body, 83 | }) 84 | if err != nil { 85 | log.Fatalf("failed to publish message: %s", err) 86 | } 87 | } 88 | }) 89 | 90 | cp := &paho.Connect{ 91 | KeepAlive: 30, 92 | CleanStart: true, 93 | ClientID: "listen1", 94 | Username: username, 95 | Password: []byte(password), 96 | } 97 | 98 | if username != "" { 99 | cp.UsernameFlag = true 100 | } 101 | if password != "" { 102 | cp.PasswordFlag = true 103 | } 104 | 105 | ca, err := c.Connect(context.Background(), cp) 106 | if err != nil { 107 | log.Fatalln(err) 108 | } 109 | if ca.ReasonCode != 0 { 110 | log.Fatalf("Failed to connect to %s : %d - %s", server, ca.ReasonCode, ca.Properties.ReasonString) 111 | } 112 | 113 | fmt.Printf("Connected to %s\n", server) 114 | 115 | _, err = c.Subscribe(context.Background(), &paho.Subscribe{ 116 | Subscriptions: map[string]paho.SubscribeOptions{ 117 | rTopic: paho.SubscribeOptions{QoS: 0}, 118 | }, 119 | }) 120 | if err != nil { 121 | log.Fatalf("failed to subscribe: %s", err) 122 | } 123 | 124 | v.Done() 125 | 126 | for { 127 | time.Sleep(1 * time.Second) 128 | } 129 | }() 130 | 131 | v.Wait() 132 | } 133 | 134 | func main() { 135 | server := flag.String("server", "127.0.0.1:1883", "The full URL of the MQTT server to connect to") 136 | rTopic := flag.String("rtopic", "rpc/request", "Topic for requests to go to") 137 | username := flag.String("username", "", "A username to authenticate to the MQTT server") 138 | password := flag.String("password", "", "Password to match username") 139 | flag.Parse() 140 | 141 | //paho.SetDebugLogger(log.New(os.Stderr, "RPC: ", log.LstdFlags)) 142 | 143 | listener(*server, *rTopic, *username, *password) 144 | 145 | conn, err := net.Dial("tcp", *server) 146 | if err != nil { 147 | log.Fatalf("Failed to connect to %s: %s", *server, err) 148 | } 149 | 150 | c := paho.NewClient(paho.ClientConfig{ 151 | Router: paho.NewSingleHandlerRouter(nil), 152 | Conn: conn, 153 | }) 154 | 155 | cp := &paho.Connect{ 156 | KeepAlive: 30, 157 | CleanStart: true, 158 | Username: *username, 159 | Password: []byte(*password), 160 | } 161 | 162 | if *username != "" { 163 | cp.UsernameFlag = true 164 | } 165 | if *password != "" { 166 | cp.PasswordFlag = true 167 | } 168 | 169 | ca, err := c.Connect(context.Background(), cp) 170 | if err != nil { 171 | log.Fatalln(err) 172 | } 173 | if ca.ReasonCode != 0 { 174 | log.Fatalf("Failed to connect to %s : %d - %s", *server, ca.ReasonCode, ca.Properties.ReasonString) 175 | } 176 | 177 | fmt.Printf("Connected to %s\n", *server) 178 | 179 | h, err := rpc.NewHandler(c) 180 | if err != nil { 181 | log.Fatal(err) 182 | } 183 | 184 | resp, err := h.Request(&paho.Publish{ 185 | Topic: *rTopic, 186 | Payload: []byte(`{"function":"mul", "param1": 10, "param2": 5}`), 187 | }) 188 | if err != nil { 189 | log.Fatal(err) 190 | } 191 | 192 | log.Printf("Received response: %s", string(resp.Payload)) 193 | } 194 | -------------------------------------------------------------------------------- /paho/cp_connect.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import "github.com/netdata/paho.golang/packets" 4 | 5 | type ( 6 | // Connect is a representation of the MQTT Connect packet 7 | Connect struct { 8 | Password []byte 9 | Username string 10 | ClientID string 11 | Properties *ConnectProperties 12 | WillMessage *WillMessage 13 | WillProperties *WillProperties 14 | KeepAlive uint16 15 | CleanStart bool 16 | UsernameFlag bool 17 | PasswordFlag bool 18 | } 19 | 20 | // ConnectProperties is a struct of the properties that can be set 21 | // for a Connect packet 22 | ConnectProperties struct { 23 | AuthData []byte 24 | AuthMethod string 25 | SessionExpiryInterval *uint32 26 | WillDelayInterval *uint32 27 | ReceiveMaximum *uint16 28 | TopicAliasMaximum *uint16 29 | MaximumQOS *byte 30 | MaximumPacketSize *uint32 31 | User map[string]string 32 | RequestProblemInfo bool 33 | RequestResponseInfo bool 34 | } 35 | ) 36 | 37 | // InitProperties is a function that takes a lower level 38 | // Properties struct and completes the properties of the Connect on 39 | // which it is called 40 | func (c *Connect) InitProperties(p *packets.Properties) { 41 | c.Properties = &ConnectProperties{ 42 | SessionExpiryInterval: p.SessionExpiryInterval, 43 | AuthMethod: p.AuthMethod, 44 | AuthData: p.AuthData, 45 | WillDelayInterval: p.WillDelayInterval, 46 | RequestResponseInfo: false, 47 | RequestProblemInfo: true, 48 | ReceiveMaximum: p.ReceiveMaximum, 49 | TopicAliasMaximum: p.TopicAliasMaximum, 50 | MaximumQOS: p.MaximumQOS, 51 | MaximumPacketSize: p.MaximumPacketSize, 52 | User: p.User, 53 | } 54 | 55 | if p.RequestResponseInfo != nil { 56 | c.Properties.RequestResponseInfo = *p.RequestProblemInfo == 1 57 | } 58 | if p.RequestProblemInfo != nil { 59 | c.Properties.RequestProblemInfo = *p.RequestProblemInfo == 1 60 | } 61 | } 62 | 63 | // InitWillProperties is a function that takes a lower level 64 | // Properties struct and completes the properties of the Will in the Connect on 65 | // which it is called 66 | func (c *Connect) InitWillProperties(p *packets.Properties) { 67 | c.WillProperties = &WillProperties{ 68 | WillDelayInterval: p.WillDelayInterval, 69 | PayloadFormat: p.PayloadFormat, 70 | MessageExpiry: p.MessageExpiry, 71 | ContentType: p.ContentType, 72 | ResponseTopic: p.ResponseTopic, 73 | CorrelationData: p.CorrelationData, 74 | User: p.User, 75 | } 76 | } 77 | 78 | // ConnectFromPacketConnect takes a packets library Connect and 79 | // returns a paho library Connect 80 | func ConnectFromPacketConnect(p *packets.Connect) *Connect { 81 | v := &Connect{ 82 | UsernameFlag: p.UsernameFlag, 83 | Username: p.Username, 84 | PasswordFlag: p.PasswordFlag, 85 | Password: p.Password, 86 | ClientID: p.ClientID, 87 | CleanStart: p.CleanStart, 88 | KeepAlive: p.KeepAlive, 89 | } 90 | v.InitProperties(p.Properties) 91 | if p.WillFlag { 92 | v.WillMessage = &WillMessage{ 93 | Retain: p.WillRetain, 94 | QoS: p.WillQOS, 95 | Topic: p.WillTopic, 96 | Payload: p.WillMessage, 97 | } 98 | v.InitWillProperties(p.WillProperties) 99 | } 100 | 101 | return v 102 | } 103 | 104 | // Packet returns a packets library Connect from the paho Connect 105 | // on which it is called 106 | func (c *Connect) Packet() *packets.Connect { 107 | v := &packets.Connect{ 108 | UsernameFlag: c.UsernameFlag, 109 | Username: c.Username, 110 | PasswordFlag: c.PasswordFlag, 111 | Password: c.Password, 112 | ClientID: c.ClientID, 113 | CleanStart: c.CleanStart, 114 | KeepAlive: c.KeepAlive, 115 | } 116 | 117 | if c.Properties != nil { 118 | v.Properties = &packets.Properties{ 119 | SessionExpiryInterval: c.Properties.SessionExpiryInterval, 120 | AuthMethod: c.Properties.AuthMethod, 121 | AuthData: c.Properties.AuthData, 122 | WillDelayInterval: c.Properties.WillDelayInterval, 123 | ReceiveMaximum: c.Properties.ReceiveMaximum, 124 | TopicAliasMaximum: c.Properties.TopicAliasMaximum, 125 | MaximumQOS: c.Properties.MaximumQOS, 126 | MaximumPacketSize: c.Properties.MaximumPacketSize, 127 | User: c.Properties.User, 128 | } 129 | if c.Properties.RequestResponseInfo { 130 | v.Properties.RequestResponseInfo = Byte(1) 131 | } 132 | if !c.Properties.RequestProblemInfo { 133 | v.Properties.RequestProblemInfo = Byte(0) 134 | } 135 | } 136 | 137 | if c.WillMessage != nil { 138 | v.WillFlag = true 139 | v.WillQOS = c.WillMessage.QoS 140 | v.WillTopic = c.WillMessage.Topic 141 | v.WillRetain = c.WillMessage.Retain 142 | v.WillMessage = c.WillMessage.Payload 143 | if c.WillProperties != nil { 144 | v.WillProperties = &packets.Properties{ 145 | WillDelayInterval: c.WillProperties.WillDelayInterval, 146 | PayloadFormat: c.WillProperties.PayloadFormat, 147 | MessageExpiry: c.WillProperties.MessageExpiry, 148 | ContentType: c.WillProperties.ContentType, 149 | ResponseTopic: c.WillProperties.ResponseTopic, 150 | CorrelationData: c.WillProperties.CorrelationData, 151 | User: c.WillProperties.User, 152 | } 153 | } 154 | } 155 | 156 | return v 157 | } 158 | 159 | type ( 160 | // WillMessage is a representation of the LWT message that can 161 | // be sent with the Connect packet 162 | WillMessage struct { 163 | Retain bool 164 | QoS byte 165 | Topic string 166 | Payload []byte 167 | } 168 | 169 | // WillProperties is a struct of the properties that can be set 170 | // for a Will in a Connect packet 171 | WillProperties struct { 172 | WillDelayInterval *uint32 173 | PayloadFormat *byte 174 | MessageExpiry *uint32 175 | ContentType string 176 | ResponseTopic string 177 | CorrelationData []byte 178 | User map[string]string 179 | } 180 | ) 181 | -------------------------------------------------------------------------------- /paho/extensions/rpc/router.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/netdata/paho.golang/packets" 8 | "github.com/netdata/paho.golang/paho" 9 | ) 10 | 11 | // MessageHandler is a type for a function that is invoked 12 | // by a Router when it has received a Publish. 13 | type MessageHandler func(*paho.Publish, func() error) 14 | 15 | // Router is an interface of the functions for a struct that is 16 | // used to handle invoking MessageHandlers depending on the 17 | // the topic the message was published on. 18 | // RegisterHandler() takes a string of the topic, and a MessageHandler 19 | // to be invoked when Publishes are received that match that topic 20 | // UnregisterHandler() takes a string of the topic to remove 21 | // MessageHandlers for 22 | // Route() takes a Publish message and determines which MessageHandlers 23 | // should be invoked 24 | type Router interface { 25 | paho.Router 26 | 27 | RegisterHandler(string, MessageHandler) 28 | UnregisterHandler(string) 29 | } 30 | 31 | // StandardRouter is a library provided implementation of a Router that 32 | // allows for unique and multiple MessageHandlers per topic 33 | type StandardRouter struct { 34 | sync.RWMutex 35 | subscriptions map[string][]MessageHandler 36 | aliases map[uint16]string 37 | } 38 | 39 | // NewStandardRouter instantiates and returns an instance of a StandardRouter 40 | func NewStandardRouter() *StandardRouter { 41 | return &StandardRouter{ 42 | subscriptions: make(map[string][]MessageHandler), 43 | aliases: make(map[uint16]string), 44 | } 45 | } 46 | 47 | // RegisterHandler is the library provided StandardRouter's 48 | // implementation of the required interface function() 49 | func (r *StandardRouter) RegisterHandler(topic string, h MessageHandler) { 50 | r.Lock() 51 | defer r.Unlock() 52 | r.subscriptions[topic] = append(r.subscriptions[topic], h) 53 | } 54 | 55 | // UnregisterHandler is the library provided StandardRouter's 56 | // implementation of the required interface function() 57 | func (r *StandardRouter) UnregisterHandler(topic string) { 58 | r.Lock() 59 | defer r.Unlock() 60 | delete(r.subscriptions, topic) 61 | } 62 | 63 | // Route is the library provided StandardRouter's implementation 64 | // of the required interface function() 65 | func (r *StandardRouter) Route(pb *packets.Publish, ack func() error) { 66 | r.RLock() 67 | defer r.RUnlock() 68 | 69 | m := PublishFromPacketPublish(pb) 70 | 71 | var topic string 72 | if pb.Properties.TopicAlias != nil { 73 | if pb.Topic != "" { 74 | //Register new alias 75 | r.aliases[*pb.Properties.TopicAlias] = pb.Topic 76 | } 77 | if t, ok := r.aliases[*pb.Properties.TopicAlias]; ok { 78 | topic = t 79 | } 80 | } else { 81 | topic = m.Topic 82 | } 83 | 84 | for route, handlers := range r.subscriptions { 85 | if match(route, topic) { 86 | for _, handler := range handlers { 87 | handler(m, ack) 88 | } 89 | } 90 | } 91 | } 92 | 93 | func match(route, topic string) bool { 94 | return route == topic || routeIncludesTopic(route, topic) 95 | } 96 | 97 | func matchDeep(route []string, topic []string) bool { 98 | if len(route) == 0 { 99 | return len(topic) == 0 100 | } 101 | 102 | if len(topic) == 0 { 103 | return route[0] == "#" 104 | } 105 | 106 | if route[0] == "#" { 107 | return true 108 | } 109 | 110 | if (route[0] == "+") || (route[0] == topic[0]) { 111 | return matchDeep(route[1:], topic[1:]) 112 | } 113 | return false 114 | } 115 | 116 | func routeIncludesTopic(route, topic string) bool { 117 | return matchDeep(routeSplit(route), topicSplit(topic)) 118 | } 119 | 120 | func routeSplit(route string) []string { 121 | if len(route) == 0 { 122 | return nil 123 | } 124 | var result []string 125 | if strings.HasPrefix(route, "$share") { 126 | result = strings.Split(route, "/")[1:] 127 | } else { 128 | result = strings.Split(route, "/") 129 | } 130 | return result 131 | } 132 | 133 | func topicSplit(topic string) []string { 134 | if len(topic) == 0 { 135 | return nil 136 | } 137 | return strings.Split(topic, "/") 138 | } 139 | 140 | // SingleHandlerRouter is a library provided implementation of a Router 141 | // that stores only a single MessageHandler and invokes this MessageHandler 142 | // for all received Publishes 143 | type SingleHandlerRouter struct { 144 | sync.Mutex 145 | aliases map[uint16]string 146 | handler MessageHandler 147 | } 148 | 149 | // NewSingleHandlerRouter instantiates and returns an instance of a SingleHandlerRouter 150 | func NewSingleHandlerRouter(h MessageHandler) *SingleHandlerRouter { 151 | return &SingleHandlerRouter{ 152 | aliases: make(map[uint16]string), 153 | handler: h, 154 | } 155 | } 156 | 157 | // RegisterHandler is the library provided SingleHandlerRouter's 158 | // implementation of the required interface function() 159 | func (s *SingleHandlerRouter) RegisterHandler(topic string, h MessageHandler) { 160 | s.handler = h 161 | } 162 | 163 | // UnregisterHandler is the library provided SingleHandlerRouter's 164 | // implementation of the required interface function() 165 | func (s *SingleHandlerRouter) UnregisterHandler(topic string) {} 166 | 167 | // Route is the library provided SingleHandlerRouter's 168 | // implementation of the required interface function() 169 | func (s *SingleHandlerRouter) Route(pb *packets.Publish, ack func() error) { 170 | m := PublishFromPacketPublish(pb) 171 | 172 | if pb.Properties.TopicAlias != nil { 173 | if pb.Topic != "" { 174 | //Register new alias 175 | s.aliases[*pb.Properties.TopicAlias] = pb.Topic 176 | } 177 | if t, ok := s.aliases[*pb.Properties.TopicAlias]; ok { 178 | m.Topic = t 179 | } 180 | } 181 | s.handler(m, ack) 182 | } 183 | 184 | // PublishFromPacketPublish takes a packets library Publish and 185 | // returns a paho library Publish 186 | func PublishFromPacketPublish(p *packets.Publish) *paho.Publish { 187 | v := &paho.Publish{ 188 | QoS: p.QoS, 189 | Retain: p.Retain, 190 | Topic: p.Topic, 191 | Payload: p.Payload, 192 | } 193 | v.InitProperties(p.Properties) 194 | 195 | return v 196 | } 197 | -------------------------------------------------------------------------------- /packets/disconnect.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Disconnect is the Variable Header definition for a Disconnect control packet 10 | type Disconnect struct { 11 | Properties *Properties 12 | ReasonCode byte 13 | } 14 | 15 | // DisconnectNormalDisconnection, etc are the list of valid disconnection reason codes. 16 | const ( 17 | DisconnectNormalDisconnection = 0x00 18 | DisconnectDisconnectWithWillMessage = 0x04 19 | DisconnectUnspecifiedError = 0x80 20 | DisconnectMalformedPacket = 0x81 21 | DisconnectProtocolError = 0x82 22 | DisconnectImplementationSpecificError = 0x83 23 | DisconnectNotAuthorized = 0x87 24 | DisconnectServerBusy = 0x89 25 | DisconnectServerShuttingDown = 0x8B 26 | DisconnectKeepAliveTimeout = 0x8D 27 | DisconnectSessionTakenOver = 0x8E 28 | DisconnectTopicFilterInvalid = 0x8F 29 | DisconnectTopicNameInvalid = 0x90 30 | DisconnectReceiveMaximumExceeded = 0x93 31 | DisconnectTopicAliasInvalid = 0x94 32 | DisconnectPacketTooLarge = 0x95 33 | DisconnectMessageRateTooHigh = 0x96 34 | DisconnectQuotaExceeded = 0x97 35 | DisconnectAdministrativeAction = 0x98 36 | DisconnectPayloadFormatInvalid = 0x99 37 | DisconnectRetainNotSupported = 0x9A 38 | DisconnectQoSNotSupported = 0x9B 39 | DisconnectUseAnotherServer = 0x9C 40 | DisconnectServerMoved = 0x9D 41 | DisconnectSharedSubscriptionNotSupported = 0x9E 42 | DisconnectConnectionRateExceeded = 0x9F 43 | DisconnectMaximumConnectTime = 0xA0 44 | DisconnectSubscriptionIdentifiersNotSupported = 0xA1 45 | DisconnectWildcardSubscriptionsNotSupported = 0xA2 46 | ) 47 | 48 | // Unpack is the implementation of the interface required function for a packet 49 | func (d *Disconnect) Unpack(r *bytes.Buffer) error { 50 | var err error 51 | d.ReasonCode, err = r.ReadByte() 52 | if err != nil { 53 | return err 54 | } 55 | 56 | err = d.Properties.Unpack(r, DISCONNECT) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // Buffers is the implementation of the interface required function for a packet 65 | func (d *Disconnect) Buffers() net.Buffers { 66 | idvp := d.Properties.Pack(DISCONNECT) 67 | propLen := encodeVBI(len(idvp)) 68 | n := net.Buffers{[]byte{d.ReasonCode}, propLen} 69 | if len(idvp) > 0 { 70 | n = append(n, idvp) 71 | } 72 | return n 73 | } 74 | 75 | // WriteTo is the implementation of the interface required function for a packet 76 | func (d *Disconnect) WriteTo(w io.Writer) (int64, error) { 77 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: DISCONNECT}} 78 | cp.Content = d 79 | 80 | return cp.WriteTo(w) 81 | } 82 | 83 | // Reason returns a string representation of the meaning of the ReasonCode 84 | func (d *Disconnect) Reason() string { 85 | switch d.ReasonCode { 86 | case 0: 87 | return "Normal disconnection - Close the connection normally. Do not send the Will Message." 88 | case 4: 89 | return "Disconnect with Will Message - The Client wishes to disconnect but requires that the Server also publishes its Will Message." 90 | case 128: 91 | return "Unspecified error - The Connection is closed but the sender either does not wish to reveal the reason, or none of the other Reason Codes apply." 92 | case 129: 93 | return "Malformed Packet - The received packet does not conform to this specification." 94 | case 130: 95 | return "Protocol Error - An unexpected or out of order packet was received." 96 | case 131: 97 | return "Implementation specific error - The packet received is valid but cannot be processed by this implementation." 98 | case 135: 99 | return "Not authorized - The request is not authorized." 100 | case 137: 101 | return "Server busy - The Server is busy and cannot continue processing requests from this Client." 102 | case 139: 103 | return "Server shutting down - The Server is shutting down." 104 | case 141: 105 | return "Keep Alive timeout - The Connection is closed because no packet has been received for 1.5 times the Keepalive time." 106 | case 142: 107 | return "Session taken over - Another Connection using the same ClientID has connected causing this Connection to be closed." 108 | case 143: 109 | return "Topic Filter invalid - The Topic Filter is correctly formed, but is not accepted by this Sever." 110 | case 144: 111 | return "Topic Name invalid - The Topic Name is correctly formed, but is not accepted by this Client or Server." 112 | case 147: 113 | return "Receive Maximum exceeded - The Client or Server has received more than Receive Maximum publication for which it has not sent PUBACK or PUBCOMP." 114 | case 148: 115 | return "Topic Alias invalid - The Client or Server has received a PUBLISH packet containing a Topic Alias which is greater than the Maximum Topic Alias it sent in the CONNECT or CONNACK packet." 116 | case 149: 117 | return "Packet too large - The packet size is greater than Maximum Packet Size for this Client or Server." 118 | case 150: 119 | return "Message rate too high - The received data rate is too high." 120 | case 151: 121 | return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." 122 | case 152: 123 | return "Administrative action - The Connection is closed due to an administrative action." 124 | case 153: 125 | return "Payload format invalid - The payload format does not match the one specified by the Payload Format Indicator." 126 | case 154: 127 | return "Retain not supported - The Server has does not support retained messages." 128 | case 155: 129 | return "QoS not supported - The Client specified a QoS greater than the QoS specified in a Maximum QoS in the CONNACK." 130 | case 156: 131 | return "Use another server - The Client should temporarily change its Server." 132 | case 157: 133 | return "Server moved - The Server is moved and the Client should permanently change its server location." 134 | case 158: 135 | return "Shared Subscription not supported - The Server does not support Shared Subscriptions." 136 | case 159: 137 | return "Connection rate exceeded - This connection is closed because the connection rate is too high." 138 | case 160: 139 | return "Maximum connect time - The maximum connection time authorized for this connection has been exceeded." 140 | case 161: 141 | return "Subscription Identifiers not supported - The Server does not support Subscription Identifiers; the subscription is not accepted." 142 | case 162: 143 | return "Wildcard subscriptions not supported - The Server does not support Wildcard subscription; the subscription is not accepted." 144 | } 145 | 146 | return "" 147 | } 148 | -------------------------------------------------------------------------------- /packets/packets.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | ) 9 | 10 | // PacketType is a type alias to byte representing the different 11 | // MQTT control packet types 12 | type PacketType byte 13 | 14 | // The following consts are the packet type number for each of the 15 | // different control packets in MQTT 16 | const ( 17 | _ PacketType = iota 18 | CONNECT 19 | CONNACK 20 | PUBLISH 21 | PUBACK 22 | PUBREC 23 | PUBREL 24 | PUBCOMP 25 | SUBSCRIBE 26 | SUBACK 27 | UNSUBSCRIBE 28 | UNSUBACK 29 | PINGREQ 30 | PINGRESP 31 | DISCONNECT 32 | AUTH 33 | ) 34 | 35 | func (p PacketType) String() string { 36 | switch p { 37 | case CONNECT: 38 | return "CONNECT" 39 | case CONNACK: 40 | return "CONNACK" 41 | case PUBLISH: 42 | return "PUBLISH" 43 | case PUBACK: 44 | return "PUBACK" 45 | case PUBREC: 46 | return "PUBREC" 47 | case PUBREL: 48 | return "PUBREL" 49 | case PUBCOMP: 50 | return "PUBCOMP" 51 | case SUBSCRIBE: 52 | return "SUBSCRIBE" 53 | case SUBACK: 54 | return "SUBACK" 55 | case UNSUBSCRIBE: 56 | return "UNSUBSCRIBE" 57 | case UNSUBACK: 58 | return "UNSUBACK" 59 | case PINGREQ: 60 | return "PINGREQ" 61 | case PINGRESP: 62 | return "PINGRESP" 63 | case DISCONNECT: 64 | return "DISCONNECT" 65 | case AUTH: 66 | return "AUTH" 67 | default: 68 | return "" 69 | } 70 | } 71 | 72 | type ( 73 | // Packet is the interface defining the unique parts of a controlpacket 74 | Packet interface { 75 | Unpack(*bytes.Buffer) error 76 | Buffers() net.Buffers 77 | WriteTo(io.Writer) (int64, error) 78 | } 79 | 80 | // FixedHeader is the definition of a control packet fixed header 81 | FixedHeader struct { 82 | remainingLength int 83 | Type PacketType 84 | Flags byte 85 | } 86 | 87 | // ControlPacket is the definition of a control packet 88 | ControlPacket struct { 89 | Content Packet 90 | FixedHeader 91 | } 92 | ) 93 | 94 | // WriteTo operates on a FixedHeader and takes the option values and produces 95 | // the wire format byte that represents these. 96 | func (f *FixedHeader) WriteTo(w io.Writer) (int64, error) { 97 | if _, err := w.Write([]byte{byte(f.Type)<<4 | f.Flags}); err != nil { 98 | return 0, err 99 | } 100 | if _, err := w.Write(encodeVBI(f.remainingLength)); err != nil { 101 | return 0, err 102 | } 103 | 104 | return 0, nil 105 | } 106 | 107 | // PacketID is a helper function that returns the value of the PacketID 108 | // field from any kind of mqtt packet in the Content element 109 | func (c *ControlPacket) PacketID() uint16 { 110 | switch r := c.Content.(type) { 111 | case *Publish: 112 | return r.PacketID 113 | case *Puback: 114 | return r.PacketID 115 | case *Pubrec: 116 | return r.PacketID 117 | case *Pubrel: 118 | return r.PacketID 119 | case *Pubcomp: 120 | return r.PacketID 121 | case *Subscribe: 122 | return r.PacketID 123 | case *Suback: 124 | return r.PacketID 125 | case *Unsubscribe: 126 | return r.PacketID 127 | case *Unsuback: 128 | return r.PacketID 129 | default: 130 | return 0 131 | } 132 | } 133 | 134 | // NewControlPacket takes a packetType and returns a pointer to a 135 | // ControlPacket where the VariableHeader field is a pointer to an 136 | // instance of a VariableHeader definition for that packetType 137 | func NewControlPacket(t PacketType) *ControlPacket { 138 | cp := &ControlPacket{FixedHeader: FixedHeader{Type: t}} 139 | switch t { 140 | case CONNECT: 141 | cp.Content = &Connect{ 142 | ProtocolName: "MQTT", 143 | ProtocolVersion: 5, 144 | Properties: &Properties{User: make(map[string]string)}, 145 | } 146 | case CONNACK: 147 | cp.Content = &Connack{Properties: &Properties{User: make(map[string]string)}} 148 | case PUBLISH: 149 | cp.Content = &Publish{Properties: &Properties{User: make(map[string]string)}} 150 | case PUBACK: 151 | cp.Content = &Puback{Properties: &Properties{User: make(map[string]string)}} 152 | case PUBREC: 153 | cp.Content = &Pubrec{Properties: &Properties{User: make(map[string]string)}} 154 | case PUBREL: 155 | cp.Flags = 2 156 | cp.Content = &Pubrel{Properties: &Properties{User: make(map[string]string)}} 157 | case PUBCOMP: 158 | cp.Content = &Pubcomp{Properties: &Properties{User: make(map[string]string)}} 159 | case SUBSCRIBE: 160 | cp.Flags = 2 161 | cp.Content = &Subscribe{ 162 | Subscriptions: make(map[string]SubOptions), 163 | Properties: &Properties{User: make(map[string]string)}, 164 | } 165 | case SUBACK: 166 | cp.Content = &Suback{Properties: &Properties{User: make(map[string]string)}} 167 | case UNSUBSCRIBE: 168 | cp.Flags = 2 169 | cp.Content = &Unsubscribe{Properties: &Properties{User: make(map[string]string)}} 170 | case UNSUBACK: 171 | cp.Content = &Unsuback{Properties: &Properties{User: make(map[string]string)}} 172 | case PINGREQ: 173 | cp.Content = &Pingreq{} 174 | case PINGRESP: 175 | cp.Content = &Pingresp{} 176 | case DISCONNECT: 177 | cp.Content = &Disconnect{Properties: &Properties{User: make(map[string]string)}} 178 | case AUTH: 179 | cp.Flags = 1 180 | cp.Content = &Auth{Properties: &Properties{User: make(map[string]string)}} 181 | default: 182 | return nil 183 | } 184 | 185 | return cp 186 | } 187 | 188 | // ReadPacket reads a control packet from a io.Reader and returns a completed 189 | // struct with the appropriate data 190 | func ReadPacket(r io.Reader) (*ControlPacket, error) { 191 | t := [1]byte{} 192 | _, err := io.ReadFull(r, t[:]) 193 | if err != nil { 194 | return nil, err 195 | } 196 | cp := NewControlPacket(PacketType(t[0] >> 4)) 197 | if cp == nil { 198 | return nil, fmt.Errorf("invalid packet type requested, %d", t[0]>>4) 199 | } 200 | cp.Flags = t[0] & 0xF 201 | if cp.Type == PUBLISH { 202 | cp.Content.(*Publish).QoS = (cp.Flags & 0x6) >> 1 203 | } 204 | vbi, err := getVBI(r) 205 | if err != nil { 206 | return nil, err 207 | } 208 | cp.remainingLength, err = decodeVBI(vbi) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | var content bytes.Buffer 214 | content.Grow(cp.remainingLength) 215 | 216 | n, err := io.CopyN(&content, r, int64(cp.remainingLength)) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | if n != int64(cp.remainingLength) { 222 | return nil, fmt.Errorf("failed to read packet, expected %d bytes, read %d", cp.remainingLength, n) 223 | } 224 | err = cp.Content.Unpack(&content) 225 | if err != nil { 226 | return nil, err 227 | } 228 | return cp, nil 229 | } 230 | 231 | // WriteTo writes a packet to an io.Writer, handling packing all the parts of 232 | // a control packet. 233 | func (c *ControlPacket) WriteTo(w io.Writer) (int64, error) { 234 | buffers := c.Content.Buffers() 235 | for _, b := range buffers { 236 | c.remainingLength += len(b) 237 | } 238 | 239 | var header bytes.Buffer 240 | if _, err := c.FixedHeader.WriteTo(&header); err != nil { 241 | return 0, err 242 | } 243 | 244 | buffers = append(net.Buffers{header.Bytes()}, buffers...) 245 | 246 | return buffers.WriteTo(w) 247 | } 248 | 249 | func encodeVBI(length int) []byte { 250 | var x int 251 | b := [4]byte{} 252 | for { 253 | digit := byte(length % 128) 254 | length /= 128 255 | if length > 0 { 256 | digit |= 0x80 257 | } 258 | b[x] = digit 259 | x++ 260 | if length == 0 { 261 | return b[:x] 262 | } 263 | } 264 | } 265 | 266 | func getVBI(r io.Reader) (*bytes.Buffer, error) { 267 | var ret bytes.Buffer 268 | digit := [1]byte{} 269 | for { 270 | _, err := io.ReadFull(r, digit[:]) 271 | if err != nil { 272 | return nil, err 273 | } 274 | ret.WriteByte(digit[0]) 275 | if digit[0] <= 0x7f { 276 | return &ret, nil 277 | } 278 | } 279 | } 280 | 281 | func decodeVBI(r *bytes.Buffer) (int, error) { 282 | var vbi uint32 283 | var multiplier uint32 284 | for { 285 | digit, err := r.ReadByte() 286 | if err != nil && err != io.EOF { 287 | return 0, err 288 | } 289 | vbi |= uint32(digit&127) << multiplier 290 | if (digit & 128) == 0 { 291 | break 292 | } 293 | multiplier += 7 294 | } 295 | return int(vbi), nil 296 | } 297 | 298 | func writeUint16(u uint16, b *bytes.Buffer) error { 299 | if err := b.WriteByte(byte(u >> 8)); err != nil { 300 | return err 301 | } 302 | return b.WriteByte(byte(u)) 303 | } 304 | 305 | func writeUint32(u uint32, b *bytes.Buffer) error { 306 | if err := b.WriteByte(byte(u >> 24)); err != nil { 307 | return err 308 | } 309 | if err := b.WriteByte(byte(u >> 16)); err != nil { 310 | return err 311 | } 312 | if err := b.WriteByte(byte(u >> 8)); err != nil { 313 | return err 314 | } 315 | return b.WriteByte(byte(u)) 316 | } 317 | 318 | func writeString(s string, b *bytes.Buffer) { 319 | writeUint16(uint16(len(s)), b) 320 | b.WriteString(s) 321 | } 322 | 323 | func writeBinary(d []byte, b *bytes.Buffer) { 324 | writeUint16(uint16(len(d)), b) 325 | b.Write(d) 326 | } 327 | 328 | func readUint16(b *bytes.Buffer) (uint16, error) { 329 | b1, err := b.ReadByte() 330 | if err != nil { 331 | return 0, err 332 | } 333 | b2, err := b.ReadByte() 334 | if err != nil { 335 | return 0, err 336 | } 337 | return (uint16(b1) << 8) | uint16(b2), nil 338 | } 339 | 340 | func readUint32(b *bytes.Buffer) (uint32, error) { 341 | b1, err := b.ReadByte() 342 | if err != nil { 343 | return 0, err 344 | } 345 | b2, err := b.ReadByte() 346 | if err != nil { 347 | return 0, err 348 | } 349 | b3, err := b.ReadByte() 350 | if err != nil { 351 | return 0, err 352 | } 353 | b4, err := b.ReadByte() 354 | if err != nil { 355 | return 0, err 356 | } 357 | return (uint32(b1) << 24) | (uint32(b2) << 16) | (uint32(b3) << 8) | uint32(b4), nil 358 | } 359 | 360 | func readBinary(b *bytes.Buffer) ([]byte, error) { 361 | size, err := readUint16(b) 362 | if err != nil { 363 | return nil, err 364 | } 365 | 366 | var s bytes.Buffer 367 | s.Grow(int(size)) 368 | if _, err := io.CopyN(&s, b, int64(size)); err != nil { 369 | return nil, err 370 | } 371 | 372 | return s.Bytes(), nil 373 | } 374 | 375 | func readString(b *bytes.Buffer) (string, error) { 376 | s, err := readBinary(b) 377 | return string(s), err 378 | } 379 | -------------------------------------------------------------------------------- /paho/client_test.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "golang.org/x/sync/semaphore" 11 | 12 | "github.com/netdata/paho.golang/packets" 13 | ) 14 | 15 | func TestNewClient(t *testing.T) { 16 | c := NewClient(ClientConfig{}) 17 | 18 | require.NotNil(t, c) 19 | require.NotNil(t, c.Persistence) 20 | require.NotNil(t, c.MIDs) 21 | 22 | assert.Equal(t, uint16(65535), c.serverProps.ReceiveMaximum) 23 | assert.Equal(t, uint8(2), c.serverProps.MaximumQoS) 24 | assert.Equal(t, uint32(0), c.serverProps.MaximumPacketSize) 25 | assert.Equal(t, uint16(0), c.serverProps.TopicAliasMaximum) 26 | assert.True(t, c.serverProps.RetainAvailable) 27 | assert.True(t, c.serverProps.WildcardSubAvailable) 28 | assert.True(t, c.serverProps.SubIDAvailable) 29 | assert.True(t, c.serverProps.SharedSubAvailable) 30 | 31 | assert.Equal(t, uint16(65535), c.clientProps.ReceiveMaximum) 32 | assert.Equal(t, uint8(2), c.clientProps.MaximumQoS) 33 | assert.Equal(t, uint32(0), c.clientProps.MaximumPacketSize) 34 | assert.Equal(t, uint16(0), c.clientProps.TopicAliasMaximum) 35 | 36 | assert.Equal(t, 10*time.Second, c.PacketTimeout) 37 | } 38 | 39 | func TestClientConnect(t *testing.T) { 40 | ts := newTestServer() 41 | ts.SetResponse(packets.CONNACK, &packets.Connack{ 42 | ReasonCode: 0, 43 | SessionPresent: false, 44 | Properties: &packets.Properties{ 45 | MaximumPacketSize: Uint32(12345), 46 | MaximumQOS: Byte(1), 47 | ReceiveMaximum: Uint16(12345), 48 | TopicAliasMaximum: Uint16(200), 49 | }, 50 | }) 51 | go ts.Run() 52 | defer ts.Stop() 53 | 54 | c := NewClient(ClientConfig{ 55 | Conn: ts.ClientConn(), 56 | }) 57 | _, err := c.Connect(context.Background(), new(Connect)) 58 | require.NoError(t, err) 59 | 60 | cp := &Connect{ 61 | KeepAlive: 30, 62 | ClientID: "testClient", 63 | CleanStart: true, 64 | Properties: &ConnectProperties{ 65 | ReceiveMaximum: Uint16(200), 66 | }, 67 | WillMessage: &WillMessage{ 68 | Topic: "will/topic", 69 | Payload: []byte("am gone"), 70 | }, 71 | WillProperties: &WillProperties{ 72 | WillDelayInterval: Uint32(200), 73 | }, 74 | } 75 | 76 | ca, err := c.Connect(context.Background(), cp) 77 | require.Nil(t, err) 78 | assert.Equal(t, uint8(0), ca.ReasonCode) 79 | 80 | time.Sleep(10 * time.Millisecond) 81 | } 82 | 83 | func TestClientSubscribe(t *testing.T) { 84 | ts := newTestServer() 85 | ts.SetResponse(packets.SUBACK, &packets.Suback{ 86 | Reasons: []byte{1, 2, 0}, 87 | Properties: &packets.Properties{}, 88 | }) 89 | go ts.Run() 90 | defer ts.Stop() 91 | 92 | c := NewClient(ClientConfig{ 93 | Conn: ts.ClientConn(), 94 | }) 95 | _, err := c.Connect(context.Background(), new(Connect)) 96 | require.NoError(t, err) 97 | 98 | s := &Subscribe{ 99 | Subscriptions: map[string]SubscribeOptions{ 100 | "test/1": {QoS: 1}, 101 | "test/2": {QoS: 2}, 102 | "test/3": {QoS: 0}, 103 | }, 104 | } 105 | 106 | sa, err := c.Subscribe(context.Background(), s) 107 | require.Nil(t, err) 108 | assert.Equal(t, []byte{1, 2, 0}, sa.Reasons) 109 | 110 | time.Sleep(10 * time.Millisecond) 111 | } 112 | 113 | func TestClientUnsubscribe(t *testing.T) { 114 | ts := newTestServer() 115 | ts.SetResponse(packets.UNSUBACK, &packets.Unsuback{ 116 | Reasons: []byte{0, 17}, 117 | Properties: &packets.Properties{}, 118 | }) 119 | go ts.Run() 120 | defer ts.Stop() 121 | 122 | c := NewClient(ClientConfig{ 123 | Conn: ts.ClientConn(), 124 | }) 125 | _, err := c.Connect(context.Background(), new(Connect)) 126 | require.NoError(t, err) 127 | 128 | u := &Unsubscribe{ 129 | Topics: []string{ 130 | "test/1", 131 | "test/2", 132 | }, 133 | } 134 | 135 | ua, err := c.Unsubscribe(context.Background(), u) 136 | require.Nil(t, err) 137 | assert.Equal(t, []byte{0, 17}, ua.Reasons) 138 | 139 | time.Sleep(10 * time.Millisecond) 140 | } 141 | 142 | func TestClientPublishQoS0(t *testing.T) { 143 | ts := newTestServer() 144 | go ts.Run() 145 | defer ts.Stop() 146 | 147 | c := NewClient(ClientConfig{ 148 | Conn: ts.ClientConn(), 149 | }) 150 | _, err := c.Connect(context.Background(), new(Connect)) 151 | require.NoError(t, err) 152 | 153 | c.serverInflight = semaphore.NewWeighted(10000) 154 | c.clientInflight = semaphore.NewWeighted(10000) 155 | 156 | p := &Publish{ 157 | Topic: "test/0", 158 | QoS: 0, 159 | Payload: []byte("test payload"), 160 | } 161 | 162 | _, err = c.Publish(context.Background(), p) 163 | require.Nil(t, err) 164 | 165 | time.Sleep(10 * time.Millisecond) 166 | } 167 | 168 | func TestClientPublishQoS1(t *testing.T) { 169 | ts := newTestServer() 170 | ts.SetResponse(packets.PUBACK, &packets.Puback{ 171 | ReasonCode: packets.PubackSuccess, 172 | Properties: &packets.Properties{}, 173 | }) 174 | go ts.Run() 175 | defer ts.Stop() 176 | 177 | c := NewClient(ClientConfig{ 178 | Conn: ts.ClientConn(), 179 | }) 180 | _, err := c.Connect(context.Background(), new(Connect)) 181 | require.NoError(t, err) 182 | 183 | c.serverInflight = semaphore.NewWeighted(10000) 184 | c.clientInflight = semaphore.NewWeighted(10000) 185 | 186 | p := &Publish{ 187 | Topic: "test/1", 188 | QoS: 1, 189 | Payload: []byte("test payload"), 190 | } 191 | 192 | pa, err := c.Publish(context.Background(), p) 193 | require.Nil(t, err) 194 | assert.Equal(t, uint8(0), pa.ReasonCode) 195 | 196 | time.Sleep(10 * time.Millisecond) 197 | } 198 | 199 | func TestClientPublishQoS2(t *testing.T) { 200 | ts := newTestServer() 201 | ts.SetResponse(packets.PUBREC, &packets.Pubrec{ 202 | ReasonCode: packets.PubrecSuccess, 203 | Properties: &packets.Properties{}, 204 | }) 205 | ts.SetResponse(packets.PUBCOMP, &packets.Pubcomp{ 206 | ReasonCode: packets.PubcompSuccess, 207 | Properties: &packets.Properties{}, 208 | }) 209 | go ts.Run() 210 | defer ts.Stop() 211 | 212 | c := NewClient(ClientConfig{ 213 | Conn: ts.ClientConn(), 214 | }) 215 | _, err := c.Connect(context.Background(), new(Connect)) 216 | require.NoError(t, err) 217 | 218 | c.serverInflight = semaphore.NewWeighted(10000) 219 | c.clientInflight = semaphore.NewWeighted(10000) 220 | 221 | p := &Publish{ 222 | Topic: "test/2", 223 | QoS: 2, 224 | Payload: []byte("test payload"), 225 | } 226 | 227 | pr, err := c.Publish(context.Background(), p) 228 | require.Nil(t, err) 229 | assert.Equal(t, uint8(0), pr.ReasonCode) 230 | 231 | time.Sleep(10 * time.Millisecond) 232 | } 233 | 234 | func TestClientReceiveQoS0(t *testing.T) { 235 | rChan := make(chan struct{}) 236 | ts := newTestServer() 237 | go ts.Run() 238 | defer ts.Stop() 239 | 240 | c := NewClient(ClientConfig{ 241 | Conn: ts.ClientConn(), 242 | Router: RouterFunc(func(p *packets.Publish, _ func() error) { 243 | assert.Equal(t, "test/0", p.Topic) 244 | assert.Equal(t, "test payload", string(p.Payload)) 245 | assert.Equal(t, byte(0), p.QoS) 246 | close(rChan) 247 | }), 248 | }) 249 | _, err := c.Connect(context.Background(), new(Connect)) 250 | require.NoError(t, err) 251 | 252 | c.serverInflight = semaphore.NewWeighted(10000) 253 | c.clientInflight = semaphore.NewWeighted(10000) 254 | 255 | err = ts.SendPacket(&packets.Publish{ 256 | Topic: "test/0", 257 | QoS: 0, 258 | Payload: []byte("test payload"), 259 | }) 260 | require.NoError(t, err) 261 | 262 | <-rChan 263 | } 264 | 265 | func TestClientReceiveQoS1(t *testing.T) { 266 | rChan := make(chan struct{}) 267 | ts := newTestServer() 268 | go ts.Run() 269 | defer ts.Stop() 270 | 271 | c := NewClient(ClientConfig{ 272 | Conn: ts.ClientConn(), 273 | Router: RouterFunc(func(p *packets.Publish, _ func() error) { 274 | assert.Equal(t, "test/1", p.Topic) 275 | assert.Equal(t, "test payload", string(p.Payload)) 276 | assert.Equal(t, byte(1), p.QoS) 277 | close(rChan) 278 | }), 279 | }) 280 | _, err := c.Connect(context.Background(), new(Connect)) 281 | require.NoError(t, err) 282 | 283 | c.serverInflight = semaphore.NewWeighted(10000) 284 | c.clientInflight = semaphore.NewWeighted(10000) 285 | 286 | err = ts.SendPacket(&packets.Publish{ 287 | Topic: "test/1", 288 | QoS: 1, 289 | Payload: []byte("test payload"), 290 | }) 291 | require.NoError(t, err) 292 | 293 | <-rChan 294 | } 295 | 296 | func TestClientReceiveQoS2(t *testing.T) { 297 | rChan := make(chan struct{}) 298 | ts := newTestServer() 299 | go ts.Run() 300 | defer ts.Stop() 301 | 302 | c := NewClient(ClientConfig{ 303 | Conn: ts.ClientConn(), 304 | Router: RouterFunc(func(p *packets.Publish, _ func() error) { 305 | assert.Equal(t, "test/2", p.Topic) 306 | assert.Equal(t, "test payload", string(p.Payload)) 307 | assert.Equal(t, byte(2), p.QoS) 308 | close(rChan) 309 | }), 310 | }) 311 | _, err := c.Connect(context.Background(), new(Connect)) 312 | require.NoError(t, err) 313 | 314 | c.serverInflight = semaphore.NewWeighted(10000) 315 | c.clientInflight = semaphore.NewWeighted(10000) 316 | 317 | err = ts.SendPacket(&packets.Publish{ 318 | Topic: "test/2", 319 | QoS: 2, 320 | Payload: []byte("test payload"), 321 | }) 322 | require.NoError(t, err) 323 | 324 | <-rChan 325 | } 326 | 327 | func TestReceiveServerDisconnect(t *testing.T) { 328 | rChan := make(chan struct{}) 329 | ts := newTestServer() 330 | go ts.Run() 331 | defer ts.Stop() 332 | 333 | c := NewClient(ClientConfig{ 334 | Conn: ts.ClientConn(), 335 | OnClose: func() { 336 | close(rChan) 337 | }, 338 | }) 339 | _, err := c.Connect(context.Background(), new(Connect)) 340 | require.NoError(t, err) 341 | 342 | c.serverInflight = semaphore.NewWeighted(10000) 343 | c.clientInflight = semaphore.NewWeighted(10000) 344 | 345 | err = ts.SendPacket(&packets.Disconnect{ 346 | ReasonCode: packets.DisconnectServerShuttingDown, 347 | Properties: &packets.Properties{ 348 | ReasonString: "GONE!", 349 | }, 350 | }) 351 | require.NoError(t, err) 352 | 353 | <-rChan 354 | } 355 | 356 | func TestAuthenticate(t *testing.T) { 357 | ts := newTestServer() 358 | ts.SetResponse(packets.AUTH, &packets.Auth{ 359 | ReasonCode: packets.AuthSuccess, 360 | }) 361 | go ts.Run() 362 | defer ts.Stop() 363 | 364 | c := NewClient(ClientConfig{ 365 | Conn: ts.ClientConn(), 366 | AuthHandler: &fakeAuth{}, 367 | }) 368 | _, err := c.Connect(context.Background(), new(Connect)) 369 | require.NoError(t, err) 370 | 371 | c.serverInflight = semaphore.NewWeighted(10000) 372 | c.clientInflight = semaphore.NewWeighted(10000) 373 | 374 | ctx, cf := context.WithTimeout(context.Background(), 5*time.Second) 375 | defer cf() 376 | ar, err := c.Authenticate(ctx, &Auth{ 377 | ReasonCode: packets.AuthReauthenticate, 378 | Properties: &AuthProperties{ 379 | AuthMethod: "TEST", 380 | AuthData: []byte("secret data"), 381 | }, 382 | }) 383 | require.Nil(t, err) 384 | assert.True(t, ar.Success) 385 | 386 | time.Sleep(10 * time.Millisecond) 387 | } 388 | -------------------------------------------------------------------------------- /packets/packets_test.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestEncodeVBI127(t *testing.T) { 14 | b := encodeVBI(127) 15 | 16 | require.Len(t, b, 1) 17 | assert.Equal(t, byte(127), b[0]) 18 | } 19 | 20 | func TestEncodeVBI128(t *testing.T) { 21 | b := encodeVBI(128) 22 | 23 | require.Len(t, b, 2) 24 | assert.Equal(t, byte(0x80), b[0]) 25 | assert.Equal(t, byte(0x01), b[1]) 26 | } 27 | 28 | func TestEncodeVBI16383(t *testing.T) { 29 | b := encodeVBI(16383) 30 | 31 | require.Len(t, b, 2) 32 | assert.Equal(t, byte(0xff), b[0]) 33 | assert.Equal(t, byte(0x7f), b[1]) 34 | } 35 | 36 | func TestEncodeVBI16384(t *testing.T) { 37 | b := encodeVBI(16384) 38 | 39 | require.Len(t, b, 3) 40 | assert.Equal(t, byte(0x80), b[0]) 41 | assert.Equal(t, byte(0x80), b[1]) 42 | assert.Equal(t, byte(0x01), b[2]) 43 | } 44 | 45 | func TestEncodeVBI2097151(t *testing.T) { 46 | b := encodeVBI(2097151) 47 | 48 | require.Len(t, b, 3) 49 | assert.Equal(t, byte(0xff), b[0]) 50 | assert.Equal(t, byte(0xff), b[1]) 51 | assert.Equal(t, byte(0x7f), b[2]) 52 | } 53 | 54 | func TestEncodeVBI2097152(t *testing.T) { 55 | b := encodeVBI(2097152) 56 | 57 | require.Len(t, b, 4) 58 | assert.Equal(t, byte(0x80), b[0]) 59 | assert.Equal(t, byte(0x80), b[1]) 60 | assert.Equal(t, byte(0x80), b[2]) 61 | assert.Equal(t, byte(0x01), b[3]) 62 | } 63 | 64 | func TestEncodeVBIMax(t *testing.T) { 65 | b := encodeVBI(268435455) 66 | 67 | require.Len(t, b, 4) 68 | assert.Equal(t, byte(0xff), b[0]) 69 | assert.Equal(t, byte(0xff), b[1]) 70 | assert.Equal(t, byte(0xff), b[2]) 71 | assert.Equal(t, byte(0x7f), b[3]) 72 | } 73 | 74 | func TestDecodeVBI12(t *testing.T) { 75 | x, err := decodeVBI(bytes.NewBuffer([]byte{0x0C})) 76 | 77 | require.Nil(t, err) 78 | assert.Equal(t, 12, x) 79 | } 80 | 81 | func TestDecodeVBI127(t *testing.T) { 82 | x, err := decodeVBI(bytes.NewBuffer([]byte{0xff})) 83 | 84 | require.Nil(t, err) 85 | assert.Equal(t, 127, x) 86 | } 87 | func TestDecodeVBI128(t *testing.T) { 88 | x, err := decodeVBI(bytes.NewBuffer([]byte{0x80, 0x01})) 89 | 90 | require.Nil(t, err) 91 | assert.Equal(t, 128, x) 92 | } 93 | func TestDecodeVBI16384(t *testing.T) { 94 | x, err := decodeVBI(bytes.NewBuffer([]byte{0x80, 0x80, 0x01})) 95 | 96 | require.Nil(t, err) 97 | assert.Equal(t, 16384, x) 98 | } 99 | func TestDecodeVBIMax(t *testing.T) { 100 | x, err := decodeVBI(bytes.NewBuffer([]byte{0xff, 0xff, 0xff, 0x7f})) 101 | 102 | require.Nil(t, err) 103 | assert.Equal(t, 268435455, x) 104 | } 105 | 106 | func TestNewControlPacketConnect(t *testing.T) { 107 | var b bytes.Buffer 108 | x := NewControlPacket(CONNECT) 109 | 110 | require.Equal(t, CONNECT, x.Type) 111 | 112 | x.Content.(*Connect).KeepAlive = 30 113 | x.Content.(*Connect).ClientID = "testClient" 114 | x.Content.(*Connect).UsernameFlag = true 115 | x.Content.(*Connect).Username = "testUser" 116 | sExpiryInterval := uint32(30) 117 | x.Content.(*Connect).Properties.SessionExpiryInterval = &sExpiryInterval 118 | 119 | _, err := x.WriteTo(&b) 120 | 121 | require.Nil(t, err) 122 | assert.Len(t, b.Bytes(), 40) 123 | } 124 | 125 | func TestReadPacketConnect(t *testing.T) { 126 | p := []byte{16, 38, 0, 4, 77, 81, 84, 84, 5, 128, 0, 30, 5, 17, 0, 0, 0, 30, 0, 10, 116, 101, 115, 116, 67, 108, 105, 101, 110, 116, 0, 8, 116, 101, 115, 116, 85, 115, 101, 114} 127 | 128 | c, err := ReadPacket(bufio.NewReader(bytes.NewReader(p))) 129 | 130 | require.Nil(t, err) 131 | assert.Equal(t, uint16(30), c.Content.(*Connect).KeepAlive) 132 | assert.Equal(t, "testClient", c.Content.(*Connect).ClientID) 133 | assert.Equal(t, true, c.Content.(*Connect).UsernameFlag) 134 | assert.Equal(t, "testUser", c.Content.(*Connect).Username) 135 | assert.Equal(t, uint32(30), *c.Content.(*Connect).Properties.SessionExpiryInterval) 136 | } 137 | 138 | func TestReadStringWriteString(t *testing.T) { 139 | var b bytes.Buffer 140 | writeString("Test string", &b) 141 | 142 | s, err := readString(&b) 143 | require.Nil(t, err) 144 | assert.Equal(t, "Test string", s) 145 | } 146 | 147 | func TestNewControlPacket(t *testing.T) { 148 | tests := []struct { 149 | name string 150 | args PacketType 151 | want *ControlPacket 152 | }{ 153 | { 154 | name: "connect", 155 | args: CONNECT, 156 | want: &ControlPacket{ 157 | FixedHeader: FixedHeader{Type: CONNECT}, 158 | Content: &Connect{ 159 | ProtocolName: "MQTT", 160 | ProtocolVersion: 5, 161 | Properties: &Properties{User: make(map[string]string)}, 162 | }, 163 | }, 164 | }, 165 | { 166 | name: "connack", 167 | args: CONNACK, 168 | want: &ControlPacket{ 169 | FixedHeader: FixedHeader{Type: CONNACK}, 170 | Content: &Connack{Properties: &Properties{User: make(map[string]string)}}, 171 | }, 172 | }, 173 | { 174 | name: "publish", 175 | args: PUBLISH, 176 | want: &ControlPacket{ 177 | FixedHeader: FixedHeader{Type: PUBLISH}, 178 | Content: &Publish{Properties: &Properties{User: make(map[string]string)}}, 179 | }, 180 | }, 181 | { 182 | name: "puback", 183 | args: PUBACK, 184 | want: &ControlPacket{ 185 | FixedHeader: FixedHeader{Type: PUBACK}, 186 | Content: &Puback{Properties: &Properties{User: make(map[string]string)}}, 187 | }, 188 | }, 189 | { 190 | name: "pubrec", 191 | args: PUBREC, 192 | want: &ControlPacket{ 193 | FixedHeader: FixedHeader{Type: PUBREC}, 194 | Content: &Pubrec{Properties: &Properties{User: make(map[string]string)}}, 195 | }, 196 | }, 197 | { 198 | name: "pubrel", 199 | args: PUBREL, 200 | want: &ControlPacket{ 201 | FixedHeader: FixedHeader{Type: PUBREL, Flags: 2}, 202 | Content: &Pubrel{Properties: &Properties{User: make(map[string]string)}}, 203 | }, 204 | }, 205 | { 206 | name: "pubcomp", 207 | args: PUBCOMP, 208 | want: &ControlPacket{ 209 | FixedHeader: FixedHeader{Type: PUBCOMP}, 210 | Content: &Pubcomp{Properties: &Properties{User: make(map[string]string)}}, 211 | }, 212 | }, 213 | { 214 | name: "subscribe", 215 | args: SUBSCRIBE, 216 | want: &ControlPacket{ 217 | FixedHeader: FixedHeader{Type: SUBSCRIBE, Flags: 2}, 218 | Content: &Subscribe{ 219 | Properties: &Properties{User: make(map[string]string)}, 220 | Subscriptions: make(map[string]SubOptions), 221 | }, 222 | }, 223 | }, 224 | { 225 | name: "suback", 226 | args: SUBACK, 227 | want: &ControlPacket{ 228 | FixedHeader: FixedHeader{Type: SUBACK}, 229 | Content: &Suback{Properties: &Properties{User: make(map[string]string)}}, 230 | }, 231 | }, 232 | { 233 | name: "unsubscribe", 234 | args: UNSUBSCRIBE, 235 | want: &ControlPacket{ 236 | FixedHeader: FixedHeader{Type: UNSUBSCRIBE, Flags: 2}, 237 | Content: &Unsubscribe{Properties: &Properties{User: make(map[string]string)}}, 238 | }, 239 | }, 240 | { 241 | name: "unsuback", 242 | args: UNSUBACK, 243 | want: &ControlPacket{ 244 | FixedHeader: FixedHeader{Type: UNSUBACK}, 245 | Content: &Unsuback{Properties: &Properties{User: make(map[string]string)}}, 246 | }, 247 | }, 248 | { 249 | name: "pingreq", 250 | args: PINGREQ, 251 | want: &ControlPacket{ 252 | FixedHeader: FixedHeader{Type: PINGREQ}, 253 | Content: &Pingreq{}, 254 | }, 255 | }, 256 | { 257 | name: "pingresp", 258 | args: PINGRESP, 259 | want: &ControlPacket{ 260 | FixedHeader: FixedHeader{Type: PINGRESP}, 261 | Content: &Pingresp{}, 262 | }, 263 | }, 264 | { 265 | name: "disconnect", 266 | args: DISCONNECT, 267 | want: &ControlPacket{ 268 | FixedHeader: FixedHeader{Type: DISCONNECT}, 269 | Content: &Disconnect{Properties: &Properties{User: make(map[string]string)}}, 270 | }, 271 | }, 272 | { 273 | name: "auth", 274 | args: AUTH, 275 | want: &ControlPacket{ 276 | FixedHeader: FixedHeader{Type: AUTH, Flags: 1}, 277 | Content: &Auth{Properties: &Properties{User: make(map[string]string)}}, 278 | }, 279 | }, 280 | { 281 | name: "dummy", 282 | args: 20, 283 | want: nil, 284 | }, 285 | } 286 | for _, tt := range tests { 287 | t.Run(tt.name, func(t *testing.T) { 288 | if got := NewControlPacket(tt.args); !reflect.DeepEqual(got, tt.want) { 289 | t.Errorf("NewControlPacket() = %v, want %v", got, tt.want) 290 | } 291 | }) 292 | } 293 | } 294 | 295 | func TestControlPacket_PacketID(t *testing.T) { 296 | type fields struct { 297 | Content Packet 298 | FixedHeader FixedHeader 299 | } 300 | tests := []struct { 301 | name string 302 | fields fields 303 | want uint16 304 | }{ 305 | { 306 | name: "publish", 307 | fields: fields{ 308 | FixedHeader: FixedHeader{Type: PUBLISH}, 309 | Content: &Publish{PacketID: 123}, 310 | }, 311 | want: 123, 312 | }, 313 | { 314 | name: "puback", 315 | fields: fields{ 316 | FixedHeader: FixedHeader{Type: PUBACK}, 317 | Content: &Puback{PacketID: 123}, 318 | }, 319 | want: 123, 320 | }, 321 | { 322 | name: "pubrel", 323 | fields: fields{ 324 | FixedHeader: FixedHeader{Type: PUBREL}, 325 | Content: &Pubrel{PacketID: 123}, 326 | }, 327 | want: 123, 328 | }, 329 | { 330 | name: "pubrec", 331 | fields: fields{ 332 | FixedHeader: FixedHeader{Type: PUBREC}, 333 | Content: &Pubrec{PacketID: 123}, 334 | }, 335 | want: 123, 336 | }, 337 | { 338 | name: "pubcomp", 339 | fields: fields{ 340 | FixedHeader: FixedHeader{Type: PUBCOMP}, 341 | Content: &Pubcomp{PacketID: 123}, 342 | }, 343 | want: 123, 344 | }, 345 | { 346 | name: "subscribe", 347 | fields: fields{ 348 | FixedHeader: FixedHeader{Type: SUBSCRIBE}, 349 | Content: &Subscribe{PacketID: 123}, 350 | }, 351 | want: 123, 352 | }, 353 | { 354 | name: "suback", 355 | fields: fields{ 356 | FixedHeader: FixedHeader{Type: SUBACK}, 357 | Content: &Suback{PacketID: 123}, 358 | }, 359 | want: 123, 360 | }, 361 | { 362 | name: "unsubscribe", 363 | fields: fields{ 364 | FixedHeader: FixedHeader{Type: UNSUBSCRIBE}, 365 | Content: &Unsubscribe{PacketID: 123}, 366 | }, 367 | want: 123, 368 | }, 369 | { 370 | name: "unsuback", 371 | fields: fields{ 372 | FixedHeader: FixedHeader{Type: UNSUBACK}, 373 | Content: &Unsuback{PacketID: 123}, 374 | }, 375 | want: 123, 376 | }, { 377 | name: "connect", 378 | fields: fields{ 379 | FixedHeader: FixedHeader{Type: CONNECT}, 380 | Content: &Connect{}, 381 | }, 382 | want: 0, 383 | }, 384 | } 385 | for _, tt := range tests { 386 | t.Run(tt.name, func(t *testing.T) { 387 | c := &ControlPacket{ 388 | Content: tt.fields.Content, 389 | FixedHeader: tt.fields.FixedHeader, 390 | } 391 | if got := c.PacketID(); got != tt.want { 392 | t.Errorf("ControlPacket.PacketID() = %v, want %v", got, tt.want) 393 | } 394 | }) 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /packets/properties.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // PropPayloadFormat, etc are the list of property codes for the 10 | // MQTT packet properties 11 | const ( 12 | PropPayloadFormat byte = 1 13 | PropMessageExpiry byte = 2 14 | PropContentType byte = 3 15 | PropResponseTopic byte = 8 16 | PropCorrelationData byte = 9 17 | PropSubscriptionIdentifier byte = 11 18 | PropSessionExpiryInterval byte = 17 19 | PropAssignedClientID byte = 18 20 | PropServerKeepAlive byte = 19 21 | PropAuthMethod byte = 21 22 | PropAuthData byte = 22 23 | PropRequestProblemInfo byte = 23 24 | PropWillDelayInterval byte = 24 25 | PropRequestResponseInfo byte = 25 26 | PropResponseInfo byte = 26 27 | PropServerReference byte = 28 28 | PropReasonString byte = 31 29 | PropReceiveMaximum byte = 33 30 | PropTopicAliasMaximum byte = 34 31 | PropTopicAlias byte = 35 32 | PropMaximumQOS byte = 36 33 | PropRetainAvailable byte = 37 34 | PropUser byte = 38 35 | PropMaximumPacketSize byte = 39 36 | PropWildcardSubAvailable byte = 40 37 | PropSubIDAvailable byte = 41 38 | PropSharedSubAvailable byte = 42 39 | ) 40 | 41 | // Properties is a struct representing the all the described properties 42 | // allowed by the MQTT protocol, determining the validity of a property 43 | // relvative to the packettype it was received in is provided by the 44 | // ValidateID function 45 | type Properties struct { 46 | // PayloadFormat indicates the format of the payload of the message 47 | // 0 is unspecified bytes 48 | // 1 is UTF8 encoded character data 49 | PayloadFormat *byte 50 | // MessageExpiry is the lifetime of the message in seconds 51 | MessageExpiry *uint32 52 | // ContentType is a UTF8 string describing the content of the message 53 | // for example it could be a MIME type 54 | ContentType string 55 | // ResponseTopic is a UTF8 string indicating the topic name to which any 56 | // response to this message should be sent 57 | ResponseTopic string 58 | // CorrelationData is binary data used to associate future response 59 | // messages with the original request message 60 | CorrelationData []byte 61 | // SubscriptionIdentifier is an identifier of the subscription to which 62 | // the Publish matched 63 | SubscriptionIdentifier *uint32 64 | // SessionExpiryInterval is the time in seconds after a client disconnects 65 | // that the server should retain the session information (subscriptions etc) 66 | SessionExpiryInterval *uint32 67 | // AssignedClientID is the server assigned client identifier in the case 68 | // that a client connected without specifying a clientID the server 69 | // generates one and returns it in the Connack 70 | AssignedClientID string 71 | // ServerKeepAlive allows the server to specify in the Connack packet 72 | // the time in seconds to be used as the keep alive value 73 | ServerKeepAlive *uint16 74 | // AuthMethod is a UTF8 string containing the name of the authentication 75 | // method to be used for extended authentication 76 | AuthMethod string 77 | // AuthData is binary data containing authentication data 78 | AuthData []byte 79 | // RequestProblemInfo is used by the Client to indicate to the server to 80 | // include the Reason String and/or User Properties in case of failures 81 | RequestProblemInfo *byte 82 | // WillDelayInterval is the number of seconds the server waits after the 83 | // point at which it would otherwise send the will message before sending 84 | // it. The client reconnecting before that time expires causes the server 85 | // to cancel sending the will 86 | WillDelayInterval *uint32 87 | // RequestResponseInfo is used by the Client to request the Server provide 88 | // Response Information in the Connack 89 | RequestResponseInfo *byte 90 | // ResponseInfo is a UTF8 encoded string that can be used as the basis for 91 | // createing a Response Topic. The way in which the Client creates a 92 | // Response Topic from the Response Information is not defined. A common 93 | // use of this is to pass a globally unique portion of the topic tree which 94 | // is reserved for this Client for at least the lifetime of its Session. This 95 | // often cannot just be a random name as both the requesting Client and the 96 | // responding Client need to be authorized to use it. It is normal to use this 97 | // as the root of a topic tree for a particular Client. For the Server to 98 | // return this information, it normally needs to be correctly configured. 99 | // Using this mechanism allows this configuration to be done once in the 100 | // Server rather than in each Client 101 | ResponseInfo string 102 | // ServerReference is a UTF8 string indicating another server the client 103 | // can use 104 | ServerReference string 105 | // ReasonString is a UTF8 string representing the reason associated with 106 | // this response, intended to be human readable for diagnostic purposes 107 | ReasonString string 108 | // ReceiveMaximum is the maximum number of QOS1 & 2 messages allowed to be 109 | // 'inflight' (not having received a PUBACK/PUBCOMP response for) 110 | ReceiveMaximum *uint16 111 | // TopicAliasMaximum is the highest value permitted as a Topic Alias 112 | TopicAliasMaximum *uint16 113 | // TopicAlias is used in place of the topic string to reduce the size of 114 | // packets for repeated messages on a topic 115 | TopicAlias *uint16 116 | // MaximumQOS is the highest QOS level permitted for a Publish 117 | MaximumQOS *byte 118 | // RetainAvailable indicates whether the server supports messages with the 119 | // retain flag set 120 | RetainAvailable *byte 121 | // User is a map of user provided properties 122 | User map[string]string 123 | // MaximumPacketSize allows the client or server to specify the maximum packet 124 | // size in bytes that they support 125 | MaximumPacketSize *uint32 126 | // WildcardSubAvailable indicates whether wildcard subscriptions are permitted 127 | WildcardSubAvailable *byte 128 | // SubIDAvailable indicates whether subscription identifiers are supported 129 | SubIDAvailable *byte 130 | // SharedSubAvailable indicates whether shared subscriptions are supported 131 | SharedSubAvailable *byte 132 | } 133 | 134 | // Pack takes all the defined properties for an Properties and produces 135 | // a slice of bytes representing the wire format for the information 136 | func (i *Properties) Pack(p PacketType) []byte { 137 | var b bytes.Buffer 138 | 139 | if i == nil { 140 | return nil 141 | } 142 | 143 | if p == PUBLISH { 144 | if i.PayloadFormat != nil { 145 | b.WriteByte(PropPayloadFormat) 146 | b.WriteByte(*i.PayloadFormat) 147 | } 148 | 149 | if i.MessageExpiry != nil { 150 | b.WriteByte(PropMessageExpiry) 151 | writeUint32(*i.MessageExpiry, &b) 152 | } 153 | 154 | if i.ContentType != "" { 155 | b.WriteByte(PropContentType) 156 | writeString(i.ContentType, &b) 157 | } 158 | 159 | if i.ResponseTopic != "" { 160 | b.WriteByte(PropResponseTopic) 161 | writeString(i.ResponseTopic, &b) 162 | } 163 | 164 | if i.CorrelationData != nil && len(i.CorrelationData) > 0 { 165 | b.WriteByte(PropCorrelationData) 166 | writeBinary(i.CorrelationData, &b) 167 | } 168 | 169 | if i.TopicAlias != nil { 170 | b.WriteByte(PropTopicAlias) 171 | writeUint16(*i.TopicAlias, &b) 172 | } 173 | } 174 | 175 | if p == PUBLISH || p == SUBSCRIBE { 176 | if i.SubscriptionIdentifier != nil { 177 | b.WriteByte(PropSubscriptionIdentifier) 178 | writeUint32(*i.SubscriptionIdentifier, &b) 179 | } 180 | } 181 | 182 | if p == CONNECT || p == CONNACK { 183 | if i.ReceiveMaximum != nil { 184 | b.WriteByte(PropReceiveMaximum) 185 | writeUint16(*i.ReceiveMaximum, &b) 186 | } 187 | 188 | if i.TopicAliasMaximum != nil { 189 | b.WriteByte(PropTopicAliasMaximum) 190 | writeUint16(*i.TopicAliasMaximum, &b) 191 | } 192 | 193 | if i.MaximumQOS != nil { 194 | b.WriteByte(PropMaximumQOS) 195 | b.WriteByte(*i.MaximumQOS) 196 | } 197 | 198 | if i.MaximumPacketSize != nil { 199 | b.WriteByte(PropMaximumPacketSize) 200 | writeUint32(*i.MaximumPacketSize, &b) 201 | } 202 | } 203 | 204 | if p == CONNACK { 205 | if i.AssignedClientID != "" { 206 | b.WriteByte(PropAssignedClientID) 207 | writeString(i.AssignedClientID, &b) 208 | } 209 | 210 | if i.ServerKeepAlive != nil { 211 | b.WriteByte(PropServerKeepAlive) 212 | writeUint16(*i.ServerKeepAlive, &b) 213 | } 214 | 215 | if i.WildcardSubAvailable != nil { 216 | b.WriteByte(PropWildcardSubAvailable) 217 | b.WriteByte(*i.WildcardSubAvailable) 218 | } 219 | 220 | if i.SubIDAvailable != nil { 221 | b.WriteByte(PropSubIDAvailable) 222 | b.WriteByte(*i.SubIDAvailable) 223 | } 224 | 225 | if i.SharedSubAvailable != nil { 226 | b.WriteByte(PropSharedSubAvailable) 227 | b.WriteByte(*i.SharedSubAvailable) 228 | } 229 | 230 | if i.RetainAvailable != nil { 231 | b.WriteByte(PropRetainAvailable) 232 | b.WriteByte(*i.RetainAvailable) 233 | } 234 | 235 | if i.ResponseInfo != "" { 236 | b.WriteByte(PropResponseInfo) 237 | writeString(i.ResponseInfo, &b) 238 | } 239 | } 240 | 241 | if p == CONNECT { 242 | if i.RequestProblemInfo != nil { 243 | b.WriteByte(PropRequestProblemInfo) 244 | b.WriteByte(*i.RequestProblemInfo) 245 | } 246 | 247 | if i.WillDelayInterval != nil { 248 | b.WriteByte(PropWillDelayInterval) 249 | writeUint32(*i.WillDelayInterval, &b) 250 | } 251 | 252 | if i.RequestResponseInfo != nil { 253 | b.WriteByte(PropRequestResponseInfo) 254 | b.WriteByte(*i.RequestResponseInfo) 255 | } 256 | } 257 | 258 | if p == CONNECT || p == DISCONNECT { 259 | if i.SessionExpiryInterval != nil { 260 | b.WriteByte(PropSessionExpiryInterval) 261 | writeUint32(*i.SessionExpiryInterval, &b) 262 | } 263 | } 264 | 265 | if p == CONNECT || p == CONNACK || p == AUTH { 266 | if i.AuthMethod != "" { 267 | b.WriteByte(PropAuthMethod) 268 | writeString(i.AuthMethod, &b) 269 | } 270 | 271 | if i.AuthData != nil && len(i.AuthData) > 0 { 272 | b.WriteByte(PropAuthData) 273 | writeBinary(i.AuthData, &b) 274 | } 275 | } 276 | 277 | if p == CONNACK || p == DISCONNECT { 278 | if i.ServerReference != "" { 279 | b.WriteByte(PropServerReference) 280 | writeString(i.ServerReference, &b) 281 | } 282 | } 283 | 284 | if p != CONNECT { 285 | if i.ReasonString != "" { 286 | b.WriteByte(PropReasonString) 287 | writeString(i.ReasonString, &b) 288 | } 289 | } 290 | 291 | for k, v := range i.User { 292 | b.WriteByte(PropUser) 293 | writeString(k, &b) 294 | writeString(v, &b) 295 | } 296 | 297 | return b.Bytes() 298 | } 299 | 300 | // Unpack takes a buffer of bytes and reads out the defined properties 301 | // filling in the appropriate entries in the struct, it returns the number 302 | // of bytes used to store the Prop data and any error in decoding them 303 | func (i *Properties) Unpack(r *bytes.Buffer, p PacketType) error { 304 | vbi, err := getVBI(r) 305 | if err != nil { 306 | return err 307 | } 308 | size, err := decodeVBI(vbi) 309 | if err != nil { 310 | return err 311 | } 312 | if size == 0 { 313 | return nil 314 | } 315 | 316 | buf := bytes.NewBuffer(r.Next(size)) 317 | for { 318 | PropType, err := buf.ReadByte() 319 | if err != nil && err != io.EOF { 320 | return err 321 | } 322 | if err == io.EOF { 323 | break 324 | } 325 | if !ValidateID(p, PropType) { 326 | return fmt.Errorf("invalid Prop type %d for packet %d", PropType, p) 327 | } 328 | switch PropType { 329 | case PropPayloadFormat: 330 | pf, err := buf.ReadByte() 331 | if err != nil { 332 | return err 333 | } 334 | i.PayloadFormat = &pf 335 | case PropMessageExpiry: 336 | pe, err := readUint32(buf) 337 | if err != nil { 338 | return err 339 | } 340 | i.MessageExpiry = &pe 341 | case PropContentType: 342 | ct, err := readString(buf) 343 | if err != nil { 344 | return err 345 | } 346 | i.ContentType = ct 347 | case PropResponseTopic: 348 | tr, err := readString(buf) 349 | if err != nil { 350 | return err 351 | } 352 | i.ResponseTopic = tr 353 | case PropCorrelationData: 354 | cd, err := readBinary(buf) 355 | if err != nil { 356 | return err 357 | } 358 | i.CorrelationData = cd 359 | case PropSubscriptionIdentifier: 360 | si, err := readUint32(buf) 361 | if err != nil { 362 | return err 363 | } 364 | i.SubscriptionIdentifier = &si 365 | case PropSessionExpiryInterval: 366 | se, err := readUint32(buf) 367 | if err != nil { 368 | return err 369 | } 370 | i.SessionExpiryInterval = &se 371 | case PropAssignedClientID: 372 | ac, err := readString(buf) 373 | if err != nil { 374 | return err 375 | } 376 | i.AssignedClientID = ac 377 | case PropServerKeepAlive: 378 | sk, err := readUint16(buf) 379 | if err != nil { 380 | return err 381 | } 382 | i.ServerKeepAlive = &sk 383 | case PropAuthMethod: 384 | am, err := readString(buf) 385 | if err != nil { 386 | return err 387 | } 388 | i.AuthMethod = am 389 | case PropAuthData: 390 | ad, err := readBinary(buf) 391 | if err != nil { 392 | return err 393 | } 394 | i.AuthData = ad 395 | case PropRequestProblemInfo: 396 | rp, err := buf.ReadByte() 397 | if err != nil { 398 | return err 399 | } 400 | i.RequestProblemInfo = &rp 401 | case PropWillDelayInterval: 402 | wd, err := readUint32(buf) 403 | if err != nil { 404 | return err 405 | } 406 | i.WillDelayInterval = &wd 407 | case PropRequestResponseInfo: 408 | rp, err := buf.ReadByte() 409 | if err != nil { 410 | return err 411 | } 412 | i.RequestResponseInfo = &rp 413 | case PropResponseInfo: 414 | ri, err := readString(buf) 415 | if err != nil { 416 | return err 417 | } 418 | i.ResponseInfo = ri 419 | case PropServerReference: 420 | sr, err := readString(buf) 421 | if err != nil { 422 | return err 423 | } 424 | i.ServerReference = sr 425 | case PropReasonString: 426 | rs, err := readString(buf) 427 | if err != nil { 428 | return err 429 | } 430 | i.ReasonString = rs 431 | case PropReceiveMaximum: 432 | rm, err := readUint16(buf) 433 | if err != nil { 434 | return err 435 | } 436 | i.ReceiveMaximum = &rm 437 | case PropTopicAliasMaximum: 438 | ta, err := readUint16(buf) 439 | if err != nil { 440 | return err 441 | } 442 | i.TopicAliasMaximum = &ta 443 | case PropTopicAlias: 444 | ta, err := readUint16(buf) 445 | if err != nil { 446 | return err 447 | } 448 | i.TopicAlias = &ta 449 | case PropMaximumQOS: 450 | mq, err := buf.ReadByte() 451 | if err != nil { 452 | return err 453 | } 454 | i.MaximumQOS = &mq 455 | case PropRetainAvailable: 456 | ra, err := buf.ReadByte() 457 | if err != nil { 458 | return err 459 | } 460 | i.RetainAvailable = &ra 461 | case PropUser: 462 | k, err := readString(buf) 463 | if err != nil { 464 | return err 465 | } 466 | v, err := readString(buf) 467 | if err != nil { 468 | return err 469 | } 470 | i.User[k] = v 471 | case PropMaximumPacketSize: 472 | mp, err := readUint32(buf) 473 | if err != nil { 474 | return err 475 | } 476 | i.MaximumPacketSize = &mp 477 | case PropWildcardSubAvailable: 478 | ws, err := buf.ReadByte() 479 | if err != nil { 480 | return err 481 | } 482 | i.WildcardSubAvailable = &ws 483 | case PropSubIDAvailable: 484 | si, err := buf.ReadByte() 485 | if err != nil { 486 | return err 487 | } 488 | i.SubIDAvailable = &si 489 | case PropSharedSubAvailable: 490 | ss, err := buf.ReadByte() 491 | if err != nil { 492 | return err 493 | } 494 | i.SharedSubAvailable = &ss 495 | default: 496 | return fmt.Errorf("unknown Prop type %d", PropType) 497 | } 498 | } 499 | 500 | return nil 501 | } 502 | 503 | // ValidProperties is a map of the various properties and the 504 | // PacketTypes that property is valid for. 505 | var ValidProperties = map[byte]map[PacketType]struct{}{ 506 | PropPayloadFormat: {PUBLISH: {}}, 507 | PropMessageExpiry: {PUBLISH: {}}, 508 | PropContentType: {PUBLISH: {}}, 509 | PropResponseTopic: {PUBLISH: {}}, 510 | PropCorrelationData: {PUBLISH: {}}, 511 | PropTopicAlias: {PUBLISH: {}}, 512 | PropSubscriptionIdentifier: {PUBLISH: {}, SUBSCRIBE: {}}, 513 | PropSessionExpiryInterval: {CONNECT: {}, DISCONNECT: {}}, 514 | PropAssignedClientID: {CONNACK: {}}, 515 | PropServerKeepAlive: {CONNACK: {}}, 516 | PropWildcardSubAvailable: {CONNACK: {}}, 517 | PropSubIDAvailable: {CONNACK: {}}, 518 | PropSharedSubAvailable: {CONNACK: {}}, 519 | PropRetainAvailable: {CONNACK: {}}, 520 | PropResponseInfo: {CONNACK: {}}, 521 | PropAuthMethod: {CONNECT: {}, CONNACK: {}, AUTH: {}}, 522 | PropAuthData: {CONNECT: {}, CONNACK: {}, AUTH: {}}, 523 | PropRequestProblemInfo: {CONNECT: {}}, 524 | PropWillDelayInterval: {CONNECT: {}}, 525 | PropRequestResponseInfo: {CONNECT: {}}, 526 | PropServerReference: {CONNACK: {}, DISCONNECT: {}}, 527 | PropReasonString: {CONNACK: {}, PUBACK: {}, PUBREC: {}, PUBREL: {}, PUBCOMP: {}, SUBACK: {}, UNSUBACK: {}, DISCONNECT: {}, AUTH: {}}, 528 | PropReceiveMaximum: {CONNECT: {}, CONNACK: {}}, 529 | PropTopicAliasMaximum: {CONNECT: {}, CONNACK: {}}, 530 | PropMaximumQOS: {CONNECT: {}, CONNACK: {}}, 531 | PropMaximumPacketSize: {CONNECT: {}, CONNACK: {}}, 532 | PropUser: {CONNECT: {}, CONNACK: {}, PUBLISH: {}, PUBACK: {}, PUBREC: {}, PUBREL: {}, PUBCOMP: {}, SUBSCRIBE: {}, UNSUBSCRIBE: {}, SUBACK: {}, UNSUBACK: {}, DISCONNECT: {}, AUTH: {}}, 533 | } 534 | 535 | // ValidateID takes a PacketType and a property name and returns 536 | // a boolean indicating if that property is valid for that 537 | // PacketType 538 | func ValidateID(p PacketType, i byte) bool { 539 | _, ok := ValidProperties[i][p] 540 | return ok 541 | } 542 | -------------------------------------------------------------------------------- /paho/client.go: -------------------------------------------------------------------------------- 1 | package paho 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "golang.org/x/sync/semaphore" 14 | 15 | "github.com/netdata/paho.golang/packets" 16 | ) 17 | 18 | var ( 19 | DefaultKeepAlive = 60 * time.Second 20 | DefaultShutdownTimeout = 10 * time.Second 21 | DefaultPacketTimeout = 10 * time.Second 22 | ) 23 | 24 | // Router is an interface that capable of handling publish packets. 25 | // 26 | // NOTE: its a Router responsibility to deal with concurrent packets processing 27 | // (if needed). 28 | type Router interface { 29 | Route(pb *packets.Publish, ack func() error) 30 | } 31 | 32 | // RouterFunc is an adapter to allow the use of ordinary functions as Router. 33 | type RouterFunc func(pb *packets.Publish, ack func() error) 34 | 35 | // Route implements Router interface. 36 | func (f RouterFunc) Route(p *packets.Publish, ack func() error) { 37 | f(p, ack) 38 | } 39 | 40 | // Auther is the interface for something that implements the extended 41 | // authentication flows in MQTT v5. 42 | type Auther interface { 43 | Authenticate(*Auth) *Auth 44 | Authenticated() 45 | } 46 | 47 | type ( 48 | // ClientConfig are the user configurable options for the client, an 49 | // instance of this struct is passed into NewClient(), not all options 50 | // are required to be set, defaults are provided for Persistence, MIDs, 51 | // PacketTimeout and Router. 52 | ClientConfig struct { 53 | Conn net.Conn 54 | MIDs MIDService 55 | AuthHandler Auther 56 | Router Router 57 | Persistence Persistence 58 | PacketTimeout time.Duration 59 | ShutdownTimeout time.Duration 60 | Trace Trace 61 | Logger func(context.Context, LogEntry) 62 | OnClose func() 63 | } 64 | // Client is the struct representing an MQTT client 65 | Client struct { 66 | ClientConfig 67 | // caCtx is used for synchronously handling the connect/connack flow 68 | // raCtx is used for handling the MQTTv5 authentication exchange. 69 | 70 | connectOnce sync.Once 71 | ca *Connack // connection ack. 72 | cerr error // connection error. 73 | 74 | mu sync.Mutex 75 | closed bool 76 | caCtx *caContext 77 | raCtx *CPContext 78 | exit chan struct{} 79 | done chan struct{} 80 | writeq chan io.WriterTo 81 | writerDone chan struct{} 82 | readerDone chan struct{} 83 | pingerDone chan struct{} 84 | pong chan struct{} 85 | serverProps CommsProperties 86 | clientProps CommsProperties 87 | serverInflight *semaphore.Weighted 88 | clientInflight *semaphore.Weighted 89 | } 90 | 91 | // CommsProperties is a struct of the communication properties that may 92 | // be set by the server in the Connack and that the client needs to be 93 | // aware of for future subscribes/publishes 94 | CommsProperties struct { 95 | MaximumPacketSize uint32 96 | ReceiveMaximum uint16 97 | TopicAliasMaximum uint16 98 | MaximumQoS byte 99 | RetainAvailable bool 100 | WildcardSubAvailable bool 101 | SubIDAvailable bool 102 | SharedSubAvailable bool 103 | } 104 | 105 | caContext struct { 106 | Context context.Context 107 | Return chan *packets.Connack 108 | } 109 | ) 110 | 111 | // NewClient is used to create a new default instance of an MQTT client. 112 | // It returns a pointer to the new client instance. 113 | // The default client uses the provided MessageID and 114 | // StandardRouter implementations, and a noop Persistence. 115 | // These should be replaced if desired before the client is connected. 116 | // client.Conn *MUST* be set to an already connected net.Conn before 117 | // Connect() is called. 118 | func NewClient(conf ClientConfig) *Client { 119 | c := &Client{ 120 | serverProps: CommsProperties{ 121 | ReceiveMaximum: 65535, 122 | MaximumQoS: 2, 123 | MaximumPacketSize: 0, 124 | TopicAliasMaximum: 0, 125 | RetainAvailable: true, 126 | WildcardSubAvailable: true, 127 | SubIDAvailable: true, 128 | SharedSubAvailable: true, 129 | }, 130 | clientProps: CommsProperties{ 131 | ReceiveMaximum: 65535, 132 | MaximumQoS: 2, 133 | MaximumPacketSize: 0, 134 | TopicAliasMaximum: 0, 135 | }, 136 | exit: make(chan struct{}), 137 | done: make(chan struct{}), 138 | writeq: make(chan io.WriterTo), 139 | writerDone: make(chan struct{}), 140 | readerDone: make(chan struct{}), 141 | pingerDone: make(chan struct{}), 142 | pong: make(chan struct{}, 1), 143 | ClientConfig: conf, 144 | } 145 | 146 | if c.Persistence == nil { 147 | c.Persistence = &noopPersistence{} 148 | } 149 | if c.MIDs == nil { 150 | c.MIDs = &MIDs{index: make(map[uint16]*CPContext)} 151 | } 152 | if c.PacketTimeout == 0 { 153 | c.PacketTimeout = DefaultPacketTimeout 154 | } 155 | if c.ShutdownTimeout == 0 { 156 | c.ShutdownTimeout = DefaultShutdownTimeout 157 | } 158 | 159 | return c 160 | } 161 | 162 | // Connect is used to connect the client to a server. It presumes that 163 | // the Client instance already has a working network connection. 164 | // The function takes a pre-prepared Connect packet, and uses that to 165 | // establish an MQTT connection. Assuming the connection completes 166 | // successfully the rest of the client is initiated and the Connack 167 | // returned. Otherwise the failure Connack (if there is one) is returned 168 | // along with an error indicating the reason for the failure to connect. 169 | func (c *Client) Connect(ctx context.Context, cp *Connect) (*Connack, error) { 170 | if c.Conn == nil { 171 | return nil, fmt.Errorf("client connection is nil") 172 | } 173 | c.connectOnce.Do(func() { 174 | defer func() { 175 | if c.cerr != nil { 176 | c.close() 177 | } 178 | }() 179 | 180 | keepalive := cp.KeepAlive 181 | if keepalive == 0 { 182 | keepalive = uint16(DefaultKeepAlive / time.Second) 183 | } 184 | if cp.Properties != nil { 185 | if cp.Properties.MaximumPacketSize != nil { 186 | c.clientProps.MaximumPacketSize = *cp.Properties.MaximumPacketSize 187 | } 188 | if cp.Properties.MaximumQOS != nil { 189 | c.clientProps.MaximumQoS = *cp.Properties.MaximumQOS 190 | } 191 | if cp.Properties.ReceiveMaximum != nil { 192 | c.clientProps.ReceiveMaximum = *cp.Properties.ReceiveMaximum 193 | } 194 | if cp.Properties.TopicAliasMaximum != nil { 195 | c.clientProps.TopicAliasMaximum = *cp.Properties.TopicAliasMaximum 196 | } 197 | } 198 | 199 | go c.writer() 200 | go c.reader() 201 | 202 | connCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) 203 | defer cf() 204 | 205 | c.caCtx = &caContext{connCtx, make(chan *packets.Connack, 1)} 206 | 207 | ccp := cp.Packet() 208 | ccp.ProtocolName = "MQTT" 209 | ccp.ProtocolVersion = 5 210 | 211 | if c.cerr = c.write(ctx, ccp); c.cerr != nil { 212 | return 213 | } 214 | 215 | var cnnap *packets.Connack 216 | select { 217 | case <-connCtx.Done(): 218 | c.logCtx(ctx, LevelTrace, "timeout waiting for CONNACK") 219 | if ctx.Err() != nil { 220 | // Parent context has been canceled. 221 | // So return the raw context error. 222 | c.cerr = ctx.Err() 223 | } else { 224 | c.cerr = ErrTimeout 225 | } 226 | return 227 | case <-c.exit: 228 | c.cerr = ErrClosed 229 | return 230 | case cnnap = <-c.caCtx.Return: 231 | } 232 | 233 | ca := ConnackFromPacketConnack(cnnap) 234 | c.ca = ca 235 | 236 | if ca.ReasonCode >= 0x80 { 237 | var reason string 238 | if ca.Properties != nil { 239 | reason = ca.Properties.ReasonString 240 | } 241 | c.cerr = fmt.Errorf("failed to connect to server: %s", reason) 242 | return 243 | } 244 | 245 | if ca.Properties != nil { 246 | if ca.Properties.ServerKeepAlive != nil { 247 | keepalive = *ca.Properties.ServerKeepAlive 248 | } 249 | //if ca.Properties.AssignedClientID != "" { 250 | // c.ClientID = ca.Properties.AssignedClientID 251 | //} 252 | if ca.Properties.ReceiveMaximum != nil { 253 | c.serverProps.ReceiveMaximum = *ca.Properties.ReceiveMaximum 254 | } 255 | if ca.Properties.MaximumQoS != nil { 256 | c.serverProps.MaximumQoS = *ca.Properties.MaximumQoS 257 | } 258 | if ca.Properties.MaximumPacketSize != nil { 259 | c.serverProps.MaximumPacketSize = *ca.Properties.MaximumPacketSize 260 | } 261 | if ca.Properties.TopicAliasMaximum != nil { 262 | c.serverProps.TopicAliasMaximum = *ca.Properties.TopicAliasMaximum 263 | } 264 | c.serverProps.RetainAvailable = ca.Properties.RetainAvailable 265 | c.serverProps.WildcardSubAvailable = ca.Properties.WildcardSubAvailable 266 | c.serverProps.SubIDAvailable = ca.Properties.SubIDAvailable 267 | c.serverProps.SharedSubAvailable = ca.Properties.SharedSubAvailable 268 | } 269 | 270 | c.serverInflight = semaphore.NewWeighted(int64(c.serverProps.ReceiveMaximum)) 271 | c.clientInflight = semaphore.NewWeighted(int64(c.clientProps.ReceiveMaximum)) 272 | 273 | go c.pinger(time.Duration(keepalive) * time.Second) 274 | }) 275 | return c.ca, c.cerr 276 | } 277 | 278 | func (c *Client) waitConnected() { 279 | var dummy bool 280 | c.connectOnce.Do(func() { 281 | dummy = true 282 | }) 283 | if dummy { 284 | panic("calling method on Client without Connect() call") 285 | } 286 | } 287 | 288 | func (c *Client) IsAlive() bool { 289 | c.waitConnected() 290 | c.mu.Lock() 291 | defer c.mu.Unlock() 292 | return !c.closed 293 | } 294 | 295 | func (c *Client) Done() <-chan struct{} { 296 | c.waitConnected() 297 | return c.done 298 | } 299 | 300 | func (c *Client) close() { 301 | c.mu.Lock() 302 | defer c.mu.Unlock() 303 | if c.closed { 304 | return 305 | } 306 | c.closed = true 307 | go func() { 308 | c.log(LevelDebug, "closing") 309 | 310 | c.waitConnected() 311 | 312 | close(c.exit) 313 | <-c.writerDone 314 | <-c.pingerDone 315 | 316 | _ = c.Conn.Close() 317 | <-c.readerDone 318 | close(c.done) 319 | 320 | if c.cerr == nil && c.OnClose != nil { 321 | // Call OnClose() only when initial connection was successful (and 322 | // callback provided). 323 | c.OnClose() 324 | } 325 | }() 326 | } 327 | 328 | func (c *Client) Shutdown(ctx context.Context) { 329 | c.waitConnected() 330 | err := c.write(ctx, packets.NewControlPacket(packets.DISCONNECT)) 331 | if err == nil { 332 | select { 333 | case <-c.readerDone: 334 | case <-time.After(c.ShutdownTimeout): 335 | } 336 | } 337 | c.Close() 338 | } 339 | 340 | func (c *Client) Close() { 341 | c.waitConnected() 342 | c.close() 343 | <-c.done 344 | } 345 | 346 | var ( 347 | ErrClosed = fmt.Errorf("paho: client closed") 348 | ErrTimeout = fmt.Errorf("paho: request timeout") 349 | ) 350 | 351 | func (c *Client) write(ctx context.Context, w io.WriterTo) (err error) { 352 | t := c.traceSend(ctx, w) 353 | defer func() { 354 | t.done(ctx, err) 355 | }() 356 | select { 357 | case <-c.exit: 358 | return ErrClosed 359 | case c.writeq <- w: 360 | return nil 361 | case <-ctx.Done(): 362 | return ctx.Err() 363 | } 364 | } 365 | 366 | func (c *Client) writer() { 367 | defer func() { 368 | c.log(LevelDebug, "writer stopped") 369 | close(c.writerDone) 370 | }() 371 | for { 372 | var w io.WriterTo 373 | select { 374 | case <-c.exit: 375 | return 376 | case w = <-c.writeq: 377 | } 378 | _, err := w.WriteTo(c.Conn) 379 | if err != nil { 380 | c.fail(context.Background(), fmt.Errorf("write packet error: %w", err)) 381 | return 382 | } 383 | } 384 | } 385 | 386 | // reader is the Client function that reads and handles incoming 387 | // packets from the server. The function is started as a goroutine 388 | // from Connect(), it exits when it receives a server initiated 389 | // Disconnect, the Stop channel is or there is an error reading 390 | // a packet from the network connection 391 | func (c *Client) reader() { 392 | ctx := context.Background() 393 | defer func() { 394 | c.logCtx(ctx, LevelDebug, "reader stopped") 395 | close(c.readerDone) 396 | }() 397 | for { 398 | t := c.traceRecv(ctx) 399 | recv, err := packets.ReadPacket(c.Conn) 400 | t.done(ctx, recv, err) 401 | if err == io.EOF { 402 | c.close() 403 | return 404 | } 405 | if err != nil { 406 | c.fail(ctx, err) 407 | return 408 | } 409 | 410 | switch recv.Type { 411 | case packets.PINGRESP: 412 | select { 413 | case <-c.pingerDone: 414 | case c.pong <- struct{}{}: 415 | } 416 | case packets.CONNACK: 417 | cnnap := recv.Content.(*packets.Connack) 418 | // NOTE: No need to acquire a lock for caCtx because it never changes 419 | if c.caCtx != nil { 420 | c.caCtx.Return <- cnnap 421 | } 422 | case packets.AUTH: 423 | ap := recv.Content.(*packets.Auth) 424 | switch ap.ReasonCode { 425 | case 0x0: 426 | if c.AuthHandler != nil { 427 | c.AuthHandler.Authenticated() 428 | } 429 | c.mu.Lock() 430 | raCtx := c.raCtx 431 | c.mu.Unlock() 432 | if raCtx != nil { 433 | raCtx.Return <- *recv 434 | } 435 | case 0x18: 436 | if c.AuthHandler != nil { 437 | pkt := c.AuthHandler.Authenticate(AuthFromPacketAuth(ap)).Packet() 438 | if err := c.write(ctx, pkt); err != nil { 439 | c.fail(ctx, err) 440 | return 441 | } 442 | } 443 | } 444 | case packets.PUBLISH: 445 | pb := recv.Content.(*packets.Publish) 446 | ack := func() error { 447 | switch pb.QoS { 448 | case 1: 449 | pa := packets.Puback{ 450 | Properties: &packets.Properties{}, 451 | PacketID: pb.PacketID, 452 | } 453 | return c.write(ctx, &pa) 454 | case 2: 455 | pr := packets.Pubrec{ 456 | Properties: &packets.Properties{}, 457 | PacketID: pb.PacketID, 458 | } 459 | return c.write(ctx, &pr) 460 | } 461 | 462 | return nil 463 | } 464 | 465 | if c.Router != nil { 466 | go c.Router.Route(pb, ack) 467 | } else { 468 | _ = ack() 469 | } 470 | case packets.PUBACK, packets.PUBCOMP, packets.SUBACK, packets.UNSUBACK: 471 | if cpCtx := c.MIDs.Get(recv.PacketID()); cpCtx != nil { 472 | c.MIDs.Free(recv.PacketID()) 473 | cpCtx.Return <- *recv 474 | } else { 475 | c.logCtx(ctx, LevelWarn, 476 | "received a response for a message ID we don't know", 477 | func(e *LogEntry) { 478 | e.ControlPacket = recv 479 | }, 480 | ) 481 | } 482 | case packets.PUBREC: 483 | if cpCtx := c.MIDs.Get(recv.PacketID()); cpCtx == nil { 484 | c.logCtx(ctx, LevelWarn, 485 | "received a response for a message ID we don't know", 486 | func(e *LogEntry) { 487 | e.ControlPacket = recv 488 | }, 489 | ) 490 | pl := packets.Pubrel{ 491 | PacketID: recv.Content.(*packets.Pubrec).PacketID, 492 | ReasonCode: 0x92, 493 | } 494 | _ = c.write(ctx, &pl) 495 | } else { 496 | pr := recv.Content.(*packets.Pubrec) 497 | if pr.ReasonCode >= 0x80 { 498 | //Received a failure code, shortcut and return 499 | c.MIDs.Free(recv.PacketID()) 500 | cpCtx.Return <- *recv 501 | } else { 502 | pl := packets.Pubrel{ 503 | PacketID: pr.PacketID, 504 | } 505 | _ = c.write(ctx, &pl) 506 | } 507 | } 508 | case packets.PUBREL: 509 | //Auto respond to pubrels unless failure code 510 | pr := recv.Content.(*packets.Pubrel) 511 | if pr.ReasonCode < 0x80 { 512 | //Received a failure code, continue 513 | continue 514 | } else { 515 | pc := packets.Pubcomp{ 516 | PacketID: pr.PacketID, 517 | } 518 | _ = c.write(ctx, &pc) 519 | } 520 | case packets.DISCONNECT: 521 | c.mu.Lock() 522 | raCtx := c.raCtx 523 | c.mu.Unlock() 524 | if raCtx != nil { 525 | raCtx.Return <- *recv 526 | } 527 | c.fail(ctx, fmt.Errorf("received server initiated disconnect")) 528 | return 529 | } 530 | } 531 | } 532 | 533 | func (c *Client) pinger(d time.Duration) { 534 | defer func() { 535 | c.log(LevelDebug, "pinger stopped") 536 | close(c.pingerDone) 537 | }() 538 | var ( 539 | ctx = context.Background() 540 | timer = time.NewTimer(d) 541 | ping = packets.NewControlPacket(packets.PINGREQ) 542 | 543 | lastPing time.Time 544 | now time.Time 545 | ) 546 | for { 547 | select { 548 | case <-c.exit: 549 | timer.Stop() 550 | return 551 | 552 | case <-c.pong: 553 | lastPing = time.Time{} 554 | continue 555 | 556 | case now = <-timer.C: 557 | // Time to ping. 558 | } 559 | if !lastPing.IsZero() && now.Sub(lastPing) > 2*d { 560 | c.fail(ctx, fmt.Errorf("no pong for %s", now.Sub(lastPing))) 561 | return 562 | } 563 | if err := c.write(ctx, ping); err != nil { 564 | continue 565 | } 566 | if lastPing.IsZero() { 567 | lastPing = now 568 | } 569 | timer.Reset(d) 570 | } 571 | } 572 | 573 | func (c *Client) fail(ctx context.Context, err error) { 574 | lvl := LevelError 575 | if errors.Is(err, context.Canceled) { 576 | lvl = LevelWarn 577 | } 578 | c.logCtx(ctx, lvl, "client failed", func(e *LogEntry) { 579 | e.Error = err 580 | }) 581 | c.close() 582 | } 583 | 584 | // Authenticate is used to initiate a reauthentication of credentials with the 585 | // server. This function sends the initial Auth packet to start the reauthentication 586 | // then relies on the client AuthHandler managing any further requests from the 587 | // server until either a successful Auth packet is passed back, or a Disconnect 588 | // is received. 589 | func (c *Client) Authenticate(ctx context.Context, a *Auth) (*AuthResponse, error) { 590 | c.waitConnected() 591 | c.logCtx(ctx, LevelTrace, "client initiated reauthentication") 592 | 593 | raCtx := &CPContext{ctx, make(chan packets.ControlPacket, 1)} 594 | 595 | c.mu.Lock() 596 | if c.raCtx != nil { 597 | c.mu.Unlock() 598 | return nil, fmt.Errorf("previous auth is still in progress") 599 | } 600 | c.raCtx = raCtx 601 | c.mu.Unlock() 602 | defer func() { 603 | c.mu.Lock() 604 | c.raCtx = nil 605 | c.mu.Unlock() 606 | }() 607 | 608 | if err := c.write(ctx, a.Packet()); err != nil { 609 | return nil, err 610 | } 611 | 612 | var rp packets.ControlPacket 613 | select { 614 | case <-ctx.Done(): 615 | c.logCtx(ctx, LevelTrace, "timeout waiting for auth to complete") 616 | return nil, ctx.Err() 617 | case <-c.exit: 618 | return nil, ErrClosed 619 | case rp = <-raCtx.Return: 620 | } 621 | 622 | switch rp.Type { 623 | case packets.AUTH: 624 | //If we've received one here it must be successful, the only way 625 | //to abort a reauth is a server initiated disconnect 626 | return AuthResponseFromPacketAuth(rp.Content.(*packets.Auth)), nil 627 | case packets.DISCONNECT: 628 | return AuthResponseFromPacketDisconnect(rp.Content.(*packets.Disconnect)), nil 629 | } 630 | 631 | return nil, fmt.Errorf("error with Auth, didn't receive Auth or Disconnect") 632 | } 633 | 634 | // Subscribe is used to send a Subscription request to the MQTT server. 635 | // It is passed a pre-prepared Subscribe packet and blocks waiting for 636 | // a response Suback, or for the timeout to fire. Any response Suback 637 | // is returned from the function, along with any errors. 638 | func (c *Client) Subscribe(ctx context.Context, s *Subscribe) (*Suback, error) { 639 | c.waitConnected() 640 | if !c.serverProps.WildcardSubAvailable { 641 | for t := range s.Subscriptions { 642 | if strings.ContainsAny(t, "#+") { 643 | // Using a wildcard in a subscription when not supported 644 | return nil, fmt.Errorf("cannot subscribe to %s, server does not support wildcards", t) 645 | } 646 | } 647 | } 648 | if !c.serverProps.SubIDAvailable && s.Properties != nil && s.Properties.SubscriptionIdentifier != nil { 649 | return nil, fmt.Errorf("cannot send subscribe with subID set, server does not support subID") 650 | } 651 | if !c.serverProps.SharedSubAvailable { 652 | for t := range s.Subscriptions { 653 | if strings.HasPrefix(t, "$share") { 654 | return nil, fmt.Errorf("cannont subscribe to %s, server does not support shared subscriptions", t) 655 | } 656 | } 657 | } 658 | 659 | c.logCtx(ctx, LevelTrace, fmt.Sprintf("subscribing to %+v", s.Subscriptions)) 660 | 661 | subCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) 662 | defer cf() 663 | cpCtx := &CPContext{subCtx, make(chan packets.ControlPacket, 1)} 664 | 665 | sp := s.Packet() 666 | var err error 667 | sp.PacketID, err = c.MIDs.Request(cpCtx) 668 | 669 | if err != nil { 670 | return nil, err 671 | } 672 | 673 | if err = c.write(ctx, sp); err != nil { 674 | return nil, err 675 | } 676 | c.logCtx(ctx, LevelTrace, "waiting for SUBACK") 677 | var sap packets.ControlPacket 678 | 679 | select { 680 | case <-subCtx.Done(): 681 | c.logCtx(ctx, LevelTrace, "timeout waiting for SUBACK") 682 | if ctx.Err() != nil { 683 | // Parent context has been canceled. 684 | // So return the raw context error. 685 | return nil, ctx.Err() 686 | } else { 687 | return nil, ErrTimeout 688 | } 689 | case <-c.exit: 690 | return nil, ErrClosed 691 | case sap = <-cpCtx.Return: 692 | } 693 | 694 | if sap.Type != packets.SUBACK { 695 | return nil, fmt.Errorf("received %d instead of Suback", sap.Type) 696 | } 697 | 698 | sa := SubackFromPacketSuback(sap.Content.(*packets.Suback)) 699 | switch { 700 | case len(sa.Reasons) == 1: 701 | if sa.Reasons[0] >= 0x80 { 702 | var reason string 703 | c.logCtx(ctx, LevelDebug, fmt.Sprintf( 704 | "received an error code in Suback: %v", sa.Reasons[0], 705 | )) 706 | if sa.Properties != nil { 707 | reason = sa.Properties.ReasonString 708 | } 709 | return sa, fmt.Errorf("failed to subscribe to topic: %s", reason) 710 | } 711 | default: 712 | for _, code := range sa.Reasons { 713 | if code >= 0x80 { 714 | c.logCtx(ctx, LevelDebug, fmt.Sprintf( 715 | "received an error code in Suback: %v", code, 716 | )) 717 | return sa, fmt.Errorf("at least one requested subscription failed") 718 | } 719 | } 720 | } 721 | 722 | return sa, nil 723 | } 724 | 725 | // Unsubscribe is used to send an Unsubscribe request to the MQTT server. 726 | // It is passed a pre-prepared Unsubscribe packet and blocks waiting for 727 | // a response Unsuback, or for the timeout to fire. Any response Unsuback 728 | // is returned from the function, along with any errors. 729 | func (c *Client) Unsubscribe(ctx context.Context, u *Unsubscribe) (*Unsuback, error) { 730 | c.waitConnected() 731 | c.logCtx(ctx, LevelTrace, fmt.Sprintf( 732 | "unsubscribing from %+v", u.Topics, 733 | )) 734 | unsubCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) 735 | defer cf() 736 | cpCtx := &CPContext{unsubCtx, make(chan packets.ControlPacket, 1)} 737 | 738 | up := u.Packet() 739 | var err error 740 | up.PacketID, err = c.MIDs.Request(cpCtx) 741 | 742 | if err != nil { 743 | return nil, err 744 | } 745 | 746 | if err = c.write(ctx, up); err != nil { 747 | return nil, err 748 | } 749 | c.logCtx(ctx, LevelTrace, "waiting for UNSUBACK") 750 | var uap packets.ControlPacket 751 | 752 | select { 753 | case <-unsubCtx.Done(): 754 | c.logCtx(ctx, LevelTrace, "timeout waiting for UNSUBACK") 755 | if ctx.Err() != nil { 756 | // Parent context has been canceled. 757 | // So return the raw context error. 758 | return nil, ctx.Err() 759 | } else { 760 | return nil, ErrTimeout 761 | } 762 | case <-c.exit: 763 | return nil, ErrClosed 764 | case uap = <-cpCtx.Return: 765 | } 766 | 767 | if uap.Type != packets.UNSUBACK { 768 | return nil, fmt.Errorf("received %d instead of Unsuback", uap.Type) 769 | } 770 | 771 | ua := UnsubackFromPacketUnsuback(uap.Content.(*packets.Unsuback)) 772 | switch { 773 | case len(ua.Reasons) == 1: 774 | if ua.Reasons[0] >= 0x80 { 775 | var reason string 776 | c.logCtx(ctx, LevelDebug, fmt.Sprintf( 777 | "received an error code in Unsuback: %v", ua.Reasons[0], 778 | )) 779 | if ua.Properties != nil { 780 | reason = ua.Properties.ReasonString 781 | } 782 | return ua, fmt.Errorf("failed to unsubscribe from topic: %s", reason) 783 | } 784 | default: 785 | for _, code := range ua.Reasons { 786 | if code >= 0x80 { 787 | c.logCtx(ctx, LevelDebug, fmt.Sprintf( 788 | "received an error code in Unsuback: %v", code, 789 | )) 790 | return ua, fmt.Errorf("at least one requested unsubscribe failed") 791 | } 792 | } 793 | } 794 | 795 | return ua, nil 796 | } 797 | 798 | // Publish is used to send a publication to the MQTT server. 799 | // It is passed a pre-prepared Publish packet and blocks waiting for 800 | // the appropriate response, or for the timeout to fire. 801 | // Any response message is returned from the function, along with any errors. 802 | func (c *Client) Publish(ctx context.Context, p *Publish) (_ *PublishResponse, err error) { 803 | c.waitConnected() 804 | if p.QoS > c.serverProps.MaximumQoS { 805 | return nil, fmt.Errorf("cannot send Publish with QoS %d, server maximum QoS is %d", p.QoS, c.serverProps.MaximumQoS) 806 | } 807 | if p.Properties != nil && p.Properties.TopicAlias != nil { 808 | if c.serverProps.TopicAliasMaximum > 0 && *p.Properties.TopicAlias > c.serverProps.TopicAliasMaximum { 809 | return nil, fmt.Errorf("cannot send publish with TopicAlias %d, server topic alias maximum is %d", *p.Properties.TopicAlias, c.serverProps.TopicAliasMaximum) 810 | } 811 | } 812 | if !c.serverProps.RetainAvailable && p.Retain { 813 | return nil, fmt.Errorf("cannot send Publish with retain flag set, server does not support retained messages") 814 | } 815 | 816 | pb := p.Packet() 817 | switch p.QoS { 818 | case 0: 819 | return c.publishQoS0(ctx, pb) 820 | case 1, 2: 821 | return c.publishQoS12(ctx, pb) 822 | } 823 | 824 | return nil, fmt.Errorf("oops") 825 | } 826 | 827 | func (c *Client) publishQoS0(ctx context.Context, pb *packets.Publish) (_ *PublishResponse, err error) { 828 | t := c.tracePublish(ctx, pb) 829 | defer func() { 830 | t.done(ctx, err) 831 | }() 832 | if err := c.write(ctx, pb); err != nil { 833 | return nil, err 834 | } 835 | return nil, nil 836 | } 837 | 838 | func (c *Client) publishQoS12(ctx context.Context, pb *packets.Publish) (_ *PublishResponse, err error) { 839 | pubCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) 840 | defer cf() 841 | 842 | cpCtx := &CPContext{pubCtx, make(chan packets.ControlPacket, 1)} 843 | 844 | pb.PacketID, err = c.MIDs.Request(cpCtx) 845 | if err != nil { 846 | return nil, err 847 | } 848 | 849 | t := c.tracePublish(ctx, pb) 850 | defer func() { 851 | t.done(ctx, err) 852 | }() 853 | 854 | if err := c.serverInflight.Acquire(pubCtx, 1); err != nil { 855 | return nil, err 856 | } 857 | if err := c.write(ctx, pb); err != nil { 858 | return nil, err 859 | } 860 | 861 | var resp packets.ControlPacket 862 | select { 863 | case <-pubCtx.Done(): 864 | c.logCtx(ctx, LevelTrace, "timeout waiting for publish response") 865 | if ctx.Err() != nil { 866 | // Parent context has been canceled. 867 | // So return the raw context error. 868 | return nil, ctx.Err() 869 | } else { 870 | return nil, ErrTimeout 871 | } 872 | case <-c.exit: 873 | return nil, ErrClosed 874 | case resp = <-cpCtx.Return: 875 | } 876 | 877 | switch pb.QoS { 878 | case 1: 879 | if resp.Type != packets.PUBACK { 880 | return nil, fmt.Errorf("received %d instead of PUBACK", resp.Type) 881 | } 882 | c.serverInflight.Release(1) 883 | 884 | pr := PublishResponseFromPuback(resp.Content.(*packets.Puback)) 885 | if pr.ReasonCode >= 0x80 { 886 | return pr, fmt.Errorf("error publishing: %s", resp.Content.(*packets.Puback).Reason()) 887 | } 888 | return pr, nil 889 | case 2: 890 | switch resp.Type { 891 | case packets.PUBCOMP: 892 | c.serverInflight.Release(1) 893 | pr := PublishResponseFromPubcomp(resp.Content.(*packets.Pubcomp)) 894 | return pr, nil 895 | case packets.PUBREC: 896 | c.serverInflight.Release(1) 897 | pr := PublishResponseFromPubrec(resp.Content.(*packets.Pubrec)) 898 | return pr, nil 899 | default: 900 | return nil, fmt.Errorf("received %d instead of PUBCOMP", resp.Type) 901 | } 902 | } 903 | 904 | return nil, fmt.Errorf("ended up with a non QoS1/2 message: %d", pb.QoS) 905 | } 906 | 907 | // Disconnect is used to send a Disconnect packet to the MQTT server 908 | // Whether or not the attempt to send the Disconnect packet fails 909 | // (and if it does this function returns any error) the network connection 910 | // is . 911 | func (c *Client) Disconnect(ctx context.Context, d *Disconnect) error { 912 | c.waitConnected() 913 | return c.write(ctx, d.Packet()) 914 | } 915 | --------------------------------------------------------------------------------