diff --git a/cmd/mqttsource/main.go b/cmd/mqttsource/main.go new file mode 100644 index 00000000000..3f4e86432c9 --- /dev/null +++ b/cmd/mqttsource/main.go @@ -0,0 +1,131 @@ +/* +Copyright 2024 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "flag" + "log" + "net" + "os" + + mqttv2 "github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/eclipse/paho.golang/paho" +) + +var ( + sink string + source string + + // CloudEvents specific parameters + eventSource string + + topic string + clientid string +) + +func init() { + flag.StringVar(&sink, "sink", "", "the host url to send messages to") + flag.StringVar(&source, "source", "", "the url to get messages from") + flag.StringVar(&eventSource, "eventSource", "", "the event-source (CloudEvents)") + + flag.StringVar(&topic, "topic", "mqtt-topic", "MQTT topic subscribe to") + flag.StringVar(&clientid, "clientid", "receiver-client-id", "MQTT source client id") +} + +func main() { + flag.Parse() + + kSink := os.Getenv("K_SINK") + if kSink != "" { + sink = kSink + } + + // "source" flag must not be empty for operation. + if source == "" { + log.Fatal("A valid MQTT broker URL must be defined.") + } + + // The event's source defaults to the MQTT broker URL. + if eventSource == "" { + eventSource = source + } + + ctx := cloudevents.ContextWithTarget(context.Background(), sink) + + conn, err := net.Dial("tcp", source) + if err != nil { + log.Fatalf("failed to connect to MQTT broker: %s", err.Error()) + } + + config := &paho.ClientConfig{ + ClientID: clientid, + Conn: conn, + } + + subscribeOpt := &paho.Subscribe{ + Subscriptions: []paho.SubscribeOptions{ + {Topic: topic, + QoS: 0}, + }, + } + + mqttReceiver, err := mqttv2.New(ctx, config, mqttv2.WithSubscribe(subscribeOpt)) + if err != nil { + log.Fatalf("failed to create protocol: %s", err.Error()) + } + defer mqttReceiver.Close(ctx) + + ceReceiver, err := cloudevents.NewClient(mqttReceiver) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + + ceClient, err := cloudevents.NewClientHTTP() + if err != nil { + log.Fatalf("Failed to create a http cloudevent client: %s", err.Error()) + } + + log.Printf("MQTT source start consuming messages from %s\n", source) + err = ceReceiver.StartReceiver(ctx, func(ctx context.Context, event cloudevents.Event) { + receive(ctx, event, ceClient) + }) + if err != nil { + log.Fatalf("failed to start receiver: %s", err) + } else { + log.Printf("MQTT source stopped\n") + } + +} + +func receive(ctx context.Context, event cloudevents.Event, c cloudevents.Client) { + log.Printf("Received event: %s", event) + data := event.Data() + newEvent := cloudevents.NewEvent(cloudevents.VersionV1) + newEvent.SetType(event.Type()) + newEvent.SetSource(eventSource) + newEvent.SetID(event.ID()) + err := newEvent.SetData(cloudevents.ApplicationJSON, data) + if err != nil { + log.Printf("Error setting event data: %v", err) + return + } + if result := c.Send(ctx, newEvent); !cloudevents.IsACK(result) { + log.Printf("Sending event to sink failed: %v", result) + } +} diff --git a/config/tools/mqttsource/mqttsource.yaml b/config/tools/mqttsource/mqttsource.yaml new file mode 100644 index 00000000000..15f00982958 --- /dev/null +++ b/config/tools/mqttsource/mqttsource.yaml @@ -0,0 +1,40 @@ +# Copyright 2021 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This is a very simple Knative Eventing Source that receive messages +# from MQTT brokers and send to sink as Cloudevents +apiVersion: sources.knative.dev/v1 +kind: ContainerSource +metadata: + name: mqttsource +spec: + template: + spec: + containers: + - image: ko://knative.dev/eventing/cmd/mqttsource + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + sink: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: event-display diff --git a/go.mod b/go.mod index 72d15ef3b00..11bcaf09de2 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,11 @@ require ( github.com/ahmetb/gen-crd-api-reference-docs v0.3.1-0.20210420163308-c1402a70e2f1 github.com/cloudevents/conformance v0.2.0 github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2 + github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 v2.0.0-20240508060731-1ed9471c98bd github.com/cloudevents/sdk-go/sql/v2 v2.15.2 github.com/cloudevents/sdk-go/v2 v2.15.2 github.com/coreos/go-oidc/v3 v3.9.0 + github.com/eclipse/paho.golang v0.12.0 github.com/go-jose/go-jose/v3 v3.0.3 github.com/golang/protobuf v1.5.4 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index a6a7fb87653..1f817681dd7 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ github.com/cloudevents/conformance v0.2.0 h1:NvSXOKlagcsOWMEbi8U7Ex/0oQ4JZE1HQ45 github.com/cloudevents/conformance v0.2.0/go.mod h1:rHKDwylBH89Rns6U3wL9ww8bg9/4GbwRCDNuyoC6bcc= github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2 h1:AbtPqiUDzKup5JpTZzO297/QXgL/TAdpdXQCNwLzlaM= github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2/go.mod h1:ZbYLE+yaEQ2j4vbRc9qzvGmg30A9LhwFt/1bSebNnbU= +github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 v2.0.0-20240508060731-1ed9471c98bd h1:MGVlnkCz/b0vjfkM5tSVLD+4oaUnYuVEjiW6lAMJ9IM= +github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 v2.0.0-20240508060731-1ed9471c98bd/go.mod h1:s+KZsVZst0bVW6vuKYb8CH49CcSJDO09+ZiIeKzJmqE= github.com/cloudevents/sdk-go/sql/v2 v2.15.2 h1:TNaTeWIbDaci89xgXbmmNVGccawQOvEfWYLWrr7Fk/k= github.com/cloudevents/sdk-go/sql/v2 v2.15.2/go.mod h1:us+PSk8OXdk8pDbRfvxy5w8ub5goKE7UP9PjKDY7TPw= github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc= @@ -111,6 +113,8 @@ github.com/dgryski/go-gk v0.0.0-20200319235926-a69029f61654/go.mod h1:qm+vckxRlD github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/eclipse/paho.golang v0.12.0 h1:EXQFJbJklDnUqW6lyAknMWRhM2NgpHxwrrL8riUmp3Q= +github.com/eclipse/paho.golang v0.12.0/go.mod h1:TSDCUivu9JnoR9Hl+H7sQMcHkejWH2/xKK1NJGtLbIE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= diff --git a/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/LICENSE b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/message.go b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/message.go new file mode 100644 index 00000000000..8dd93854564 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/message.go @@ -0,0 +1,119 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package mqtt_paho + +import ( + "bytes" + "context" + "strings" + + "github.com/cloudevents/sdk-go/v2/binding" + "github.com/cloudevents/sdk-go/v2/binding/format" + "github.com/cloudevents/sdk-go/v2/binding/spec" + "github.com/eclipse/paho.golang/paho" +) + +const ( + prefix = "ce-" + contentType = "Content-Type" +) + +var specs = spec.WithPrefix(prefix) + +// Message represents a MQTT message. +// This message *can* be read several times safely +type Message struct { + internal *paho.Publish + version spec.Version + format format.Format +} + +// Check if Message implements binding.Message +var ( + _ binding.Message = (*Message)(nil) + _ binding.MessageMetadataReader = (*Message)(nil) +) + +func NewMessage(msg *paho.Publish) *Message { + var f format.Format + var v spec.Version + if msg.Properties != nil { + // Use properties.User["Content-type"] to determine if message is structured + if s := msg.Properties.User.Get(contentType); format.IsFormat(s) { + f = format.Lookup(s) + } else if s := msg.Properties.User.Get(specs.PrefixedSpecVersionName()); s != "" { + v = specs.Version(s) + } + } + return &Message{ + internal: msg, + version: v, + format: f, + } +} + +func (m *Message) ReadEncoding() binding.Encoding { + if m.version != nil { + return binding.EncodingBinary + } + if m.format != nil { + return binding.EncodingStructured + } + return binding.EncodingUnknown +} + +func (m *Message) ReadStructured(ctx context.Context, encoder binding.StructuredWriter) error { + if m.version != nil { + return binding.ErrNotStructured + } + if m.format == nil { + return binding.ErrNotStructured + } + return encoder.SetStructuredEvent(ctx, m.format, bytes.NewReader(m.internal.Payload)) +} + +func (m *Message) ReadBinary(ctx context.Context, encoder binding.BinaryWriter) (err error) { + if m.format != nil { + return binding.ErrNotBinary + } + + for _, userProperty := range m.internal.Properties.User { + if strings.HasPrefix(userProperty.Key, prefix) { + attr := m.version.Attribute(userProperty.Key) + if attr != nil { + err = encoder.SetAttribute(attr, userProperty.Value) + } else { + err = encoder.SetExtension(strings.TrimPrefix(userProperty.Key, prefix), userProperty.Value) + } + } else if userProperty.Key == contentType { + err = encoder.SetAttribute(m.version.AttributeFromKind(spec.DataContentType), string(userProperty.Value)) + } + if err != nil { + return + } + } + + if m.internal.Payload != nil { + return encoder.SetData(bytes.NewBuffer(m.internal.Payload)) + } + return nil +} + +func (m *Message) Finish(error) error { + return nil +} + +func (m *Message) GetAttribute(k spec.Kind) (spec.Attribute, interface{}) { + attr := m.version.AttributeFromKind(k) + if attr != nil { + return attr, m.internal.Properties.User.Get(prefix + attr.Name()) + } + return nil, nil +} + +func (m *Message) GetExtension(name string) interface{} { + return m.internal.Properties.User.Get(prefix + name) +} diff --git a/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/option.go b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/option.go new file mode 100644 index 00000000000..955a1621907 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/option.go @@ -0,0 +1,48 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package mqtt_paho + +import ( + "fmt" + + "github.com/eclipse/paho.golang/paho" +) + +// Option is the function signature required to be considered an mqtt_paho.Option. +type Option func(*Protocol) error + +// WithConnect sets the paho.Connect configuration for the client. This option is not required. +func WithConnect(connOpt *paho.Connect) Option { + return func(p *Protocol) error { + if connOpt == nil { + return fmt.Errorf("the paho.Connect option must not be nil") + } + p.connOption = connOpt + return nil + } +} + +// WithPublish sets the paho.Publish configuration for the client. This option is required if you want to send messages. +func WithPublish(publishOpt *paho.Publish) Option { + return func(p *Protocol) error { + if publishOpt == nil { + return fmt.Errorf("the paho.Publish option must not be nil") + } + p.publishOption = publishOpt + return nil + } +} + +// WithSubscribe sets the paho.Subscribe configuration for the client. This option is required if you want to receive messages. +func WithSubscribe(subscribeOpt *paho.Subscribe) Option { + return func(p *Protocol) error { + if subscribeOpt == nil { + return fmt.Errorf("the paho.Subscribe option must not be nil") + } + p.subscribeOption = subscribeOpt + return nil + } +} diff --git a/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/protocol.go b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/protocol.go new file mode 100644 index 00000000000..261fc6c378e --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/protocol.go @@ -0,0 +1,155 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package mqtt_paho + +import ( + "context" + "fmt" + "io" + "sync" + + "github.com/cloudevents/sdk-go/v2/binding" + "github.com/cloudevents/sdk-go/v2/protocol" + "github.com/eclipse/paho.golang/paho" + + cecontext "github.com/cloudevents/sdk-go/v2/context" +) + +type Protocol struct { + client *paho.Client + config *paho.ClientConfig + connOption *paho.Connect + publishOption *paho.Publish + subscribeOption *paho.Subscribe + + // receiver + incoming chan *paho.Publish + // inOpen + openerMutex sync.Mutex + + closeChan chan struct{} +} + +var ( + _ protocol.Sender = (*Protocol)(nil) + _ protocol.Opener = (*Protocol)(nil) + _ protocol.Receiver = (*Protocol)(nil) + _ protocol.Closer = (*Protocol)(nil) +) + +func New(ctx context.Context, config *paho.ClientConfig, opts ...Option) (*Protocol, error) { + if config == nil { + return nil, fmt.Errorf("the paho.ClientConfig must not be nil") + } + + p := &Protocol{ + client: paho.NewClient(*config), + // default connect option + connOption: &paho.Connect{ + KeepAlive: 30, + CleanStart: true, + }, + incoming: make(chan *paho.Publish), + closeChan: make(chan struct{}), + } + if err := p.applyOptions(opts...); err != nil { + return nil, err + } + + // Connect to the MQTT broker + connAck, err := p.client.Connect(ctx, p.connOption) + if err != nil { + return nil, err + } + if connAck.ReasonCode != 0 { + return nil, fmt.Errorf("failed to connect to %q : %d - %q", p.client.Conn.RemoteAddr(), connAck.ReasonCode, + connAck.Properties.ReasonString) + } + + return p, nil +} + +func (p *Protocol) applyOptions(opts ...Option) error { + for _, fn := range opts { + if err := fn(p); err != nil { + return err + } + } + return nil +} + +func (p *Protocol) Send(ctx context.Context, m binding.Message, transformers ...binding.Transformer) error { + if p.publishOption == nil { + return fmt.Errorf("the paho.Publish option must not be nil") + } + + var err error + defer m.Finish(err) + + msg := p.publishOption + if cecontext.TopicFrom(ctx) != "" { + msg.Topic = cecontext.TopicFrom(ctx) + cecontext.WithTopic(ctx, "") + } + + err = WritePubMessage(ctx, m, msg, transformers...) + if err != nil { + return err + } + + _, err = p.client.Publish(ctx, msg) + if err != nil { + return err + } + return err +} + +func (p *Protocol) OpenInbound(ctx context.Context) error { + if p.subscribeOption == nil { + return fmt.Errorf("the paho.Subscribe option must not be nil") + } + + p.openerMutex.Lock() + defer p.openerMutex.Unlock() + + logger := cecontext.LoggerFrom(ctx) + + p.client.Router = paho.NewSingleHandlerRouter(func(m *paho.Publish) { + p.incoming <- m + }) + + logger.Infof("subscribing to topics: %v", p.subscribeOption.Subscriptions) + _, err := p.client.Subscribe(ctx, p.subscribeOption) + if err != nil { + return err + } + + // Wait until external or internal context done + select { + case <-ctx.Done(): + case <-p.closeChan: + } + return p.client.Disconnect(&paho.Disconnect{ReasonCode: 0}) +} + +// Receive implements Receiver.Receive +func (p *Protocol) Receive(ctx context.Context) (binding.Message, error) { + select { + case m, ok := <-p.incoming: + if !ok { + return nil, io.EOF + } + msg := NewMessage(m) + return msg, nil + case <-ctx.Done(): + return nil, io.EOF + } +} + +func (p *Protocol) Close(ctx context.Context) error { + close(p.closeChan) + return nil +} diff --git a/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/write_message.go b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/write_message.go new file mode 100644 index 00000000000..a4b87f4aa48 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2/write_message.go @@ -0,0 +1,133 @@ +/* +Copyright 2023 The CloudEvents Authors +SPDX-License-Identifier: Apache-2.0 +*/ + +package mqtt_paho + +import ( + "bytes" + "context" + "io" + + "github.com/cloudevents/sdk-go/v2/binding" + "github.com/cloudevents/sdk-go/v2/binding/format" + "github.com/cloudevents/sdk-go/v2/binding/spec" + "github.com/cloudevents/sdk-go/v2/types" + "github.com/eclipse/paho.golang/paho" +) + +// WritePubMessage fills the provided pubMessage with the message m. +// Using context you can tweak the encoding processing (more details on binding.Write documentation). +func WritePubMessage(ctx context.Context, m binding.Message, pubMessage *paho.Publish, transformers ...binding.Transformer) error { + structuredWriter := (*pubMessageWriter)(pubMessage) + binaryWriter := (*pubMessageWriter)(pubMessage) + + _, err := binding.Write( + ctx, + m, + structuredWriter, + binaryWriter, + transformers..., + ) + return err +} + +type pubMessageWriter paho.Publish + +var ( + _ binding.StructuredWriter = (*pubMessageWriter)(nil) + _ binding.BinaryWriter = (*pubMessageWriter)(nil) +) + +func (b *pubMessageWriter) SetStructuredEvent(ctx context.Context, f format.Format, event io.Reader) error { + if b.Properties == nil { + b.Properties = &paho.PublishProperties{ + User: make([]paho.UserProperty, 0), + } + } + b.Properties.User.Add(contentType, f.MediaType()) + var buf bytes.Buffer + _, err := io.Copy(&buf, event) + if err != nil { + return err + } + b.Payload = buf.Bytes() + return nil +} + +func (b *pubMessageWriter) Start(ctx context.Context) error { + if b.Properties == nil { + b.Properties = &paho.PublishProperties{} + } + // the UserProperties of publish message is used to load event extensions + b.Properties.User = make([]paho.UserProperty, 0) + return nil +} + +func (b *pubMessageWriter) End(ctx context.Context) error { + return nil +} + +func (b *pubMessageWriter) SetData(reader io.Reader) error { + buf, ok := reader.(*bytes.Buffer) + if !ok { + buf = new(bytes.Buffer) + _, err := io.Copy(buf, reader) + if err != nil { + return err + } + } + b.Payload = buf.Bytes() + return nil +} + +func (b *pubMessageWriter) SetAttribute(attribute spec.Attribute, value interface{}) error { + if attribute.Kind() == spec.DataContentType { + if value == nil { + b.removeProperty(contentType) + } + s, err := types.Format(value) + if err != nil { + return err + } + if err := b.addProperty(contentType, s); err != nil { + return err + } + } else { + if value == nil { + b.removeProperty(prefix + attribute.Name()) + } + return b.addProperty(prefix+attribute.Name(), value) + } + return nil +} + +func (b *pubMessageWriter) SetExtension(name string, value interface{}) error { + if value == nil { + b.removeProperty(prefix + name) + } + return b.addProperty(prefix+name, value) +} + +func (b *pubMessageWriter) removeProperty(key string) { + for i, v := range b.Properties.User { + if v.Key == key { + b.Properties.User = append(b.Properties.User[:i], b.Properties.User[i+1:]...) + break + } + } +} + +func (b *pubMessageWriter) addProperty(key string, value interface{}) error { + s, err := types.Format(value) + if err != nil { + return err + } + + b.Properties.User = append(b.Properties.User, paho.UserProperty{ + Key: key, + Value: s, + }) + return nil +} diff --git a/vendor/github.com/eclipse/paho.golang/LICENSE b/vendor/github.com/eclipse/paho.golang/LICENSE new file mode 100644 index 00000000000..d3087e4c540 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/vendor/github.com/eclipse/paho.golang/packets/auth.go b/vendor/github.com/eclipse/paho.golang/packets/auth.go new file mode 100644 index 00000000000..56237e00c43 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/auth.go @@ -0,0 +1,77 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" +) + +// Auth is the Variable Header definition for a Auth control packet +type Auth struct { + Properties *Properties + ReasonCode byte +} + +// AuthSuccess is the return code for successful authentication +const ( + AuthSuccess = 0x00 + AuthContinueAuthentication = 0x18 + AuthReauthenticate = 0x19 +) + +func (a *Auth) String() string { + var b strings.Builder + + fmt.Fprintf(&b, "AUTH: ReasonCode:%X", a.ReasonCode) + if a.Properties != nil { + fmt.Fprintf(&b, " Properties:\n%s", a.Properties) + } else { + fmt.Fprint(&b, "\n") + } + + return b.String() +} + +// Unpack is the implementation of the interface required function for a packet +func (a *Auth) Unpack(r *bytes.Buffer) error { + var err error + + success := r.Len() == 0 + noProps := r.Len() == 1 + if !success { + a.ReasonCode, err = r.ReadByte() + if err != nil { + return err + } + + if !noProps { + err = a.Properties.Unpack(r, AUTH) + if err != nil { + return err + } + } + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (a *Auth) Buffers() net.Buffers { + idvp := a.Properties.Pack(AUTH) + propLen := encodeVBI(len(idvp)) + n := net.Buffers{[]byte{a.ReasonCode}, propLen} + if len(idvp) > 0 { + n = append(n, idvp) + } + return n +} + +// WriteTo is the implementation of the interface required function for a packet +func (a *Auth) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: AUTH}} + cp.Content = a + + return cp.WriteTo(w) +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/connack.go b/vendor/github.com/eclipse/paho.golang/packets/connack.go new file mode 100644 index 00000000000..aa171eaba83 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/connack.go @@ -0,0 +1,145 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" +) + +// Connack is the Variable Header definition for a connack control packet +type Connack struct { + Properties *Properties + ReasonCode byte + SessionPresent bool +} + +const ( + ConnackSuccess = 0x00 + ConnackUnspecifiedError = 0x80 + ConnackMalformedPacket = 0x81 + ConnackProtocolError = 0x82 + ConnackImplementationSpecificError = 0x83 + ConnackUnsupportedProtocolVersion = 0x84 + ConnackInvalidClientID = 0x85 + ConnackBadUsernameOrPassword = 0x86 + ConnackNotAuthorized = 0x87 + ConnackServerUnavailable = 0x88 + ConnackServerBusy = 0x89 + ConnackBanned = 0x8A + ConnackBadAuthenticationMethod = 0x8C + ConnackTopicNameInvalid = 0x90 + ConnackPacketTooLarge = 0x95 + ConnackQuotaExceeded = 0x97 + ConnackPayloadFormatInvalid = 0x99 + ConnackRetainNotSupported = 0x9A + ConnackQoSNotSupported = 0x9B + ConnackUseAnotherServer = 0x9C + ConnackServerMoved = 0x9D + ConnackConnectionRateExceeded = 0x9F +) + +func (c *Connack) String() string { + return fmt.Sprintf("CONNACK: ReasonCode:%d SessionPresent:%t\nProperties:\n%s", c.ReasonCode, c.SessionPresent, c.Properties) +} + +//Unpack is the implementation of the interface required function for a packet +func (c *Connack) Unpack(r *bytes.Buffer) error { + connackFlags, err := r.ReadByte() + if err != nil { + return err + } + c.SessionPresent = connackFlags&0x01 > 0 + + c.ReasonCode, err = r.ReadByte() + if err != nil { + return err + } + + err = c.Properties.Unpack(r, CONNACK) + if err != nil { + return err + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (c *Connack) Buffers() net.Buffers { + var header bytes.Buffer + + if c.SessionPresent { + header.WriteByte(1) + } else { + header.WriteByte(0) + } + header.WriteByte(c.ReasonCode) + + idvp := c.Properties.Pack(CONNACK) + propLen := encodeVBI(len(idvp)) + + n := net.Buffers{header.Bytes(), propLen} + if len(idvp) > 0 { + n = append(n, idvp) + } + + return n +} + +// WriteTo is the implementation of the interface required function for a packet +func (c *Connack) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: CONNACK}} + cp.Content = c + + return cp.WriteTo(w) +} + +// Reason returns a string representation of the meaning of the ReasonCode +func (c *Connack) Reason() string { + switch c.ReasonCode { + case 0: + return "Success - The Connection is accepted." + case 128: + return "Unspecified error - The Server does not wish to reveal the reason for the failure, or none of the other Reason Codes apply." + case 129: + return "Malformed Packet - Data within the CONNECT packet could not be correctly parsed." + case 130: + return "Protocol Error - Data in the CONNECT packet does not conform to this specification." + case 131: + return "Implementation specific error - The CONNECT is valid but is not accepted by this Server." + case 132: + return "Unsupported Protocol Version - The Server does not support the version of the MQTT protocol requested by the Client." + case 133: + return "Client Identifier not valid - The Client Identifier is a valid string but is not allowed by the Server." + case 134: + return "Bad User Name or Password - The Server does not accept the User Name or Password specified by the Client" + case 135: + return "Not authorized - The Client is not authorized to connect." + case 136: + return "Server unavailable - The MQTT Server is not available." + case 137: + return "Server busy - The Server is busy. Try again later." + case 138: + return "Banned - This Client has been banned by administrative action. Contact the server administrator." + case 140: + return "Bad authentication method - The authentication method is not supported or does not match the authentication method currently in use." + case 144: + return "Topic Name invalid - The Will Topic Name is not malformed, but is not accepted by this Server." + case 149: + return "Packet too large - The CONNECT packet exceeded the maximum permissible size." + case 151: + return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." + case 154: + return "Retain not supported - The Server does not support retained messages, and Will Retain was set to 1." + case 155: + return "QoS not supported - The Server does not support the QoS set in Will QoS." + case 156: + return "Use another server - The Client should temporarily use another server." + case 157: + return "Server moved - The Client should permanently use another server." + case 159: + return "Connection rate exceeded - The connection rate limit has been exceeded." + } + + return "" +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/connect.go b/vendor/github.com/eclipse/paho.golang/packets/connect.go new file mode 100644 index 00000000000..31340f6bdb9 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/connect.go @@ -0,0 +1,189 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" +) + +// Connect is the Variable Header definition for a connect control packet +type Connect struct { + WillMessage []byte + Password []byte + Username string + ProtocolName string + ClientID string + WillTopic string + Properties *Properties + WillProperties *Properties + KeepAlive uint16 + ProtocolVersion byte + WillQOS byte + PasswordFlag bool + UsernameFlag bool + WillRetain bool + WillFlag bool + CleanStart bool +} + +func (c *Connect) String() string { + var b strings.Builder + + fmt.Fprintf(&b, "CONNECT: ProtocolName:%s ProtocolVersion:%d ClientID:%s KeepAlive:%d CleanStart:%t", c.ProtocolName, c.ProtocolVersion, c.ClientID, c.KeepAlive, c.CleanStart) + if c.UsernameFlag { + fmt.Fprintf(&b, " Username:%s", c.Username) + } + if c.PasswordFlag { + fmt.Fprintf(&b, " Password:%s", c.Password) + } + fmt.Fprint(&b, "\n") + if c.WillFlag { + fmt.Fprintf(&b, " WillTopic:%s WillQOS:%d WillRetain:%t WillMessage:\n%s\n", c.WillTopic, c.WillQOS, c.WillRetain, c.WillMessage) + if c.WillProperties != nil { + fmt.Fprintf(&b, "WillProperties:\n%s", c.WillProperties) + } + } + if c.Properties != nil { + fmt.Fprintf(&b, "Properties:\n%s", c.Properties) + } + + return b.String() +} + +// PackFlags takes the Connect flags and packs them into the single byte +// representation used on the wire by MQTT +func (c *Connect) PackFlags() (f byte) { + if c.UsernameFlag { + f |= 0x01 << 7 + } + if c.PasswordFlag { + f |= 0x01 << 6 + } + if c.WillFlag { + f |= 0x01 << 2 + f |= c.WillQOS << 3 + if c.WillRetain { + f |= 0x01 << 5 + } + } + if c.CleanStart { + f |= 0x01 << 1 + } + return +} + +// UnpackFlags takes the wire byte representing the connect options flags +// and fills out the appropriate variables in the struct +func (c *Connect) UnpackFlags(b byte) { + c.CleanStart = 1&(b>>1) > 0 + c.WillFlag = 1&(b>>2) > 0 + c.WillQOS = 3 & (b >> 3) + c.WillRetain = 1&(b>>5) > 0 + c.PasswordFlag = 1&(b>>6) > 0 + c.UsernameFlag = 1&(b>>7) > 0 +} + +//Unpack is the implementation of the interface required function for a packet +func (c *Connect) Unpack(r *bytes.Buffer) error { + var err error + + if c.ProtocolName, err = readString(r); err != nil { + return err + } + + if c.ProtocolVersion, err = r.ReadByte(); err != nil { + return err + } + + flags, err := r.ReadByte() + if err != nil { + return err + } + c.UnpackFlags(flags) + + if c.KeepAlive, err = readUint16(r); err != nil { + return err + } + + err = c.Properties.Unpack(r, CONNECT) + if err != nil { + return err + } + + c.ClientID, err = readString(r) + if err != nil { + return err + } + + if c.WillFlag { + c.WillProperties = &Properties{} + err = c.WillProperties.Unpack(r, CONNECT) + if err != nil { + return err + } + c.WillTopic, err = readString(r) + if err != nil { + return err + } + c.WillMessage, err = readBinary(r) + if err != nil { + return err + } + } + + if c.UsernameFlag { + c.Username, err = readString(r) + if err != nil { + return err + } + } + + if c.PasswordFlag { + c.Password, err = readBinary(r) + if err != nil { + return err + } + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (c *Connect) Buffers() net.Buffers { + var cp bytes.Buffer + + writeString(c.ProtocolName, &cp) + cp.WriteByte(c.ProtocolVersion) + cp.WriteByte(c.PackFlags()) + writeUint16(c.KeepAlive, &cp) + idvp := c.Properties.Pack(CONNECT) + encodeVBIdirect(len(idvp), &cp) + cp.Write(idvp) + + writeString(c.ClientID, &cp) + if c.WillFlag { + willIdvp := c.WillProperties.Pack(CONNECT) + encodeVBIdirect(len(willIdvp), &cp) + cp.Write(willIdvp) + writeString(c.WillTopic, &cp) + writeBinary(c.WillMessage, &cp) + } + if c.UsernameFlag { + writeString(c.Username, &cp) + } + if c.PasswordFlag { + writeBinary(c.Password, &cp) + } + + return net.Buffers{cp.Bytes()} +} + +// WriteTo is the implementation of the interface required function for a packet +func (c *Connect) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: CONNECT}} + cp.Content = c + + return cp.WriteTo(w) +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/disconnect.go b/vendor/github.com/eclipse/paho.golang/packets/disconnect.go new file mode 100644 index 00000000000..9180207a610 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/disconnect.go @@ -0,0 +1,152 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" +) + +// Disconnect is the Variable Header definition for a Disconnect control packet +type Disconnect struct { + Properties *Properties + ReasonCode byte +} + +func (d *Disconnect) String() string { + return fmt.Sprintf("DISCONNECT: ReasonCode:%X Properties\n%s", d.ReasonCode, d.Properties) +} + +// DisconnectNormalDisconnection, etc are the list of valid disconnection reason codes. +const ( + DisconnectNormalDisconnection = 0x00 + DisconnectDisconnectWithWillMessage = 0x04 + DisconnectUnspecifiedError = 0x80 + DisconnectMalformedPacket = 0x81 + DisconnectProtocolError = 0x82 + DisconnectImplementationSpecificError = 0x83 + DisconnectNotAuthorized = 0x87 + DisconnectServerBusy = 0x89 + DisconnectServerShuttingDown = 0x8B + DisconnectKeepAliveTimeout = 0x8D + DisconnectSessionTakenOver = 0x8E + DisconnectTopicFilterInvalid = 0x8F + DisconnectTopicNameInvalid = 0x90 + DisconnectReceiveMaximumExceeded = 0x93 + DisconnectTopicAliasInvalid = 0x94 + DisconnectPacketTooLarge = 0x95 + DisconnectMessageRateTooHigh = 0x96 + DisconnectQuotaExceeded = 0x97 + DisconnectAdministrativeAction = 0x98 + DisconnectPayloadFormatInvalid = 0x99 + DisconnectRetainNotSupported = 0x9A + DisconnectQoSNotSupported = 0x9B + DisconnectUseAnotherServer = 0x9C + DisconnectServerMoved = 0x9D + DisconnectSharedSubscriptionNotSupported = 0x9E + DisconnectConnectionRateExceeded = 0x9F + DisconnectMaximumConnectTime = 0xA0 + DisconnectSubscriptionIdentifiersNotSupported = 0xA1 + DisconnectWildcardSubscriptionsNotSupported = 0xA2 +) + +// Unpack is the implementation of the interface required function for a packet +func (d *Disconnect) Unpack(r *bytes.Buffer) error { + var err error + d.ReasonCode, err = r.ReadByte() + if err != nil { + return err + } + + err = d.Properties.Unpack(r, DISCONNECT) + if err != nil { + return err + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (d *Disconnect) Buffers() net.Buffers { + idvp := d.Properties.Pack(DISCONNECT) + propLen := encodeVBI(len(idvp)) + n := net.Buffers{[]byte{d.ReasonCode}, propLen} + if len(idvp) > 0 { + n = append(n, idvp) + } + return n +} + +// WriteTo is the implementation of the interface required function for a packet +func (d *Disconnect) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: DISCONNECT}} + cp.Content = d + + return cp.WriteTo(w) +} + +// Reason returns a string representation of the meaning of the ReasonCode +func (d *Disconnect) Reason() string { + switch d.ReasonCode { + case 0: + return "Normal disconnection - Close the connection normally. Do not send the Will Message." + case 4: + return "Disconnect with Will Message - The Client wishes to disconnect but requires that the Server also publishes its Will Message." + case 128: + 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." + case 129: + return "Malformed Packet - The received packet does not conform to this specification." + case 130: + return "Protocol Error - An unexpected or out of order packet was received." + case 131: + return "Implementation specific error - The packet received is valid but cannot be processed by this implementation." + case 135: + return "Not authorized - The request is not authorized." + case 137: + return "Server busy - The Server is busy and cannot continue processing requests from this Client." + case 139: + return "Server shutting down - The Server is shutting down." + case 141: + return "Keep Alive timeout - The Connection is closed because no packet has been received for 1.5 times the Keepalive time." + case 142: + return "Session taken over - Another Connection using the same ClientID has connected causing this Connection to be closed." + case 143: + return "Topic Filter invalid - The Topic Filter is correctly formed, but is not accepted by this Sever." + case 144: + return "Topic Name invalid - The Topic Name is correctly formed, but is not accepted by this Client or Server." + case 147: + return "Receive Maximum exceeded - The Client or Server has received more than Receive Maximum publication for which it has not sent PUBACK or PUBCOMP." + case 148: + 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." + case 149: + return "Packet too large - The packet size is greater than Maximum Packet Size for this Client or Server." + case 150: + return "Message rate too high - The received data rate is too high." + case 151: + return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." + case 152: + return "Administrative action - The Connection is closed due to an administrative action." + case 153: + return "Payload format invalid - The payload format does not match the one specified by the Payload Format Indicator." + case 154: + return "Retain not supported - The Server has does not support retained messages." + case 155: + return "QoS not supported - The Client specified a QoS greater than the QoS specified in a Maximum QoS in the CONNACK." + case 156: + return "Use another server - The Client should temporarily change its Server." + case 157: + return "Server moved - The Server is moved and the Client should permanently change its server location." + case 158: + return "Shared Subscription not supported - The Server does not support Shared Subscriptions." + case 159: + return "Connection rate exceeded - This connection is closed because the connection rate is too high." + case 160: + return "Maximum connect time - The maximum connection time authorized for this connection has been exceeded." + case 161: + return "Subscription Identifiers not supported - The Server does not support Subscription Identifiers; the subscription is not accepted." + case 162: + return "Wildcard subscriptions not supported - The Server does not support Wildcard subscription; the subscription is not accepted." + } + + return "" +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/packets.go b/vendor/github.com/eclipse/paho.golang/packets/packets.go new file mode 100644 index 00000000000..4f023e015e7 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/packets.go @@ -0,0 +1,495 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "sync" +) + +// PacketType is a type alias to byte representing the different +// MQTT control packet types +// type PacketType byte + +// The following consts are the packet type number for each of the +// different control packets in MQTT +const ( + _ byte = iota + CONNECT + CONNACK + PUBLISH + PUBACK + PUBREC + PUBREL + PUBCOMP + SUBSCRIBE + SUBACK + UNSUBSCRIBE + UNSUBACK + PINGREQ + PINGRESP + DISCONNECT + AUTH +) + +type ( + // Packet is the interface defining the unique parts of a controlpacket + Packet interface { + Unpack(*bytes.Buffer) error + Buffers() net.Buffers + WriteTo(io.Writer) (int64, error) + } + + // FixedHeader is the definition of a control packet fixed header + FixedHeader struct { + remainingLength int + Type byte + Flags byte + } + + // ControlPacket is the definition of a control packet + ControlPacket struct { + Content Packet + FixedHeader + } +) + +// NewThreadSafeConn wraps net.Conn with a mutex. ControlPacket uses it in +// WriteTo method to ensure parallel writes are thread-Safe. +func NewThreadSafeConn(c net.Conn) net.Conn { + type threadSafeConn struct { + net.Conn + sync.Locker + } + + return &threadSafeConn{ + Conn: c, + Locker: &sync.Mutex{}, + } +} + +// WriteTo operates on a FixedHeader and takes the option values and produces +// the wire format byte that represents these. +func (f *FixedHeader) WriteTo(w io.Writer) (int64, error) { + if _, err := w.Write([]byte{byte(f.Type)<<4 | f.Flags}); err != nil { + return 0, err + } + if _, err := w.Write(encodeVBI(f.remainingLength)); err != nil { + return 0, err + } + + return 0, nil +} + +// PacketID is a helper function that returns the value of the PacketID +// field from any kind of mqtt packet in the Content element +func (c *ControlPacket) PacketID() uint16 { + switch r := c.Content.(type) { + case *Publish: + return r.PacketID + case *Puback: + return r.PacketID + case *Pubrec: + return r.PacketID + case *Pubrel: + return r.PacketID + case *Pubcomp: + return r.PacketID + case *Subscribe: + return r.PacketID + case *Suback: + return r.PacketID + case *Unsubscribe: + return r.PacketID + case *Unsuback: + return r.PacketID + default: + return 0 + } +} + +// PacketType returns the packet type as a string +func (c *ControlPacket) PacketType() string { + return [...]string{ + "", + "CONNECT", + "CONNACK", + "PUBLISH", + "PUBACK", + "PUBREC", + "PUBREL", + "PUBCOMP", + "SUBSCRIBE", + "SUBACK", + "UNSUBSCRIBE", + "UNSUBACK", + "PINGREQ", + "PINGRESP", + "DISCONNECT", + "AUTH", + }[c.FixedHeader.Type] +} + +// String implements fmt.Stringer (mainly for debugging purposes) +func (c *ControlPacket) String() string { + switch p := c.Content.(type) { + case *Connect: + return p.String() + case *Connack: + return p.String() + case *Publish: + return p.String() + case *Puback: + return p.String() + case *Pubrec: + return p.String() + case *Pubrel: + return p.String() + case *Pubcomp: + return p.String() + case *Subscribe: + return p.String() + case *Suback: + return p.String() + case *Unsubscribe: + return p.String() + case *Unsuback: + return p.String() + case *Pingreq: + return p.String() + case *Pingresp: + return p.String() + case *Disconnect: + return p.String() + case *Auth: + return p.String() + default: + return fmt.Sprintf("Unknown packet type: %d", c.Type) + } +} + +// NewControlPacket takes a packetType and returns a pointer to a +// ControlPacket where the VariableHeader field is a pointer to an +// instance of a VariableHeader definition for that packetType +func NewControlPacket(t byte) *ControlPacket { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: t}} + switch t { + case CONNECT: + cp.Content = &Connect{ + ProtocolName: "MQTT", + ProtocolVersion: 5, + Properties: &Properties{}, + } + case CONNACK: + cp.Content = &Connack{Properties: &Properties{}} + case PUBLISH: + cp.Content = &Publish{Properties: &Properties{}} + case PUBACK: + cp.Content = &Puback{Properties: &Properties{}} + case PUBREC: + cp.Content = &Pubrec{Properties: &Properties{}} + case PUBREL: + cp.Flags = 2 + cp.Content = &Pubrel{Properties: &Properties{}} + case PUBCOMP: + cp.Content = &Pubcomp{Properties: &Properties{}} + case SUBSCRIBE: + cp.Flags = 2 + cp.Content = &Subscribe{Properties: &Properties{}} + case SUBACK: + cp.Content = &Suback{Properties: &Properties{}} + case UNSUBSCRIBE: + cp.Flags = 2 + cp.Content = &Unsubscribe{Properties: &Properties{}} + case UNSUBACK: + cp.Content = &Unsuback{Properties: &Properties{}} + case PINGREQ: + cp.Content = &Pingreq{} + case PINGRESP: + cp.Content = &Pingresp{} + case DISCONNECT: + cp.Content = &Disconnect{Properties: &Properties{}} + case AUTH: + cp.Flags = 1 + cp.Content = &Auth{Properties: &Properties{}} + default: + return nil + } + return cp +} + +// ReadPacket reads a control packet from a io.Reader and returns a completed +// struct with the appropriate data +func ReadPacket(r io.Reader) (*ControlPacket, error) { + t := [1]byte{} + _, err := io.ReadFull(r, t[:]) + if err != nil { + return nil, err + } + // cp := NewControlPacket(PacketType(t[0] >> 4)) + // if cp == nil { + // return nil, fmt.Errorf("invalid packet type requested, %d", t[0]>>4) + // } + + pt := t[0] >> 4 + cp := &ControlPacket{FixedHeader: FixedHeader{Type: pt}} + switch pt { + case CONNECT: + cp.Content = &Connect{ + ProtocolName: "MQTT", + ProtocolVersion: 5, + Properties: &Properties{}, + } + case CONNACK: + cp.Content = &Connack{Properties: &Properties{}} + case PUBLISH: + cp.Content = &Publish{Properties: &Properties{}} + case PUBACK: + cp.Content = &Puback{Properties: &Properties{}} + case PUBREC: + cp.Content = &Pubrec{Properties: &Properties{}} + case PUBREL: + cp.Flags = 2 + cp.Content = &Pubrel{Properties: &Properties{}} + case PUBCOMP: + cp.Content = &Pubcomp{Properties: &Properties{}} + case SUBSCRIBE: + cp.Flags = 2 + cp.Content = &Subscribe{Properties: &Properties{}} + case SUBACK: + cp.Content = &Suback{Properties: &Properties{}} + case UNSUBSCRIBE: + cp.Flags = 2 + cp.Content = &Unsubscribe{Properties: &Properties{}} + case UNSUBACK: + cp.Content = &Unsuback{Properties: &Properties{}} + case PINGREQ: + cp.Content = &Pingreq{} + case PINGRESP: + cp.Content = &Pingresp{} + case DISCONNECT: + cp.Content = &Disconnect{Properties: &Properties{}} + case AUTH: + cp.Flags = 1 + cp.Content = &Auth{Properties: &Properties{}} + default: + return nil, fmt.Errorf("unknown packet type %d requested", pt) + } + + cp.Flags = t[0] & 0xF + if cp.Type == PUBLISH { // Publish is the only packet with flags in the fixed header + cp.Content.(*Publish).QoS = (cp.Flags >> 1) & 0x3 + cp.Content.(*Publish).Duplicate = cp.Flags&(1<<3) != 0 + cp.Content.(*Publish).Retain = cp.Flags&1 != 0 + } + vbi, err := getVBI(r) + if err != nil { + return nil, err + } + cp.remainingLength, err = decodeVBI(vbi) + if err != nil { + return nil, err + } + + var content bytes.Buffer + content.Grow(cp.remainingLength) + + n, err := io.CopyN(&content, r, int64(cp.remainingLength)) + if err != nil { + return nil, err + } + + if n != int64(cp.remainingLength) { + return nil, fmt.Errorf("failed to read packet, expected %d bytes, read %d", cp.remainingLength, n) + } + err = cp.Content.Unpack(&content) + if err != nil { + return nil, err + } + return cp, nil +} + +// WriteTo writes a packet to an io.Writer, handling packing all the parts of +// a control packet. +func (c *ControlPacket) WriteTo(w io.Writer) (int64, error) { + c.remainingLength = 0 // ignore previous remainingLength (if any) + buffers := c.Content.Buffers() + for _, b := range buffers { + c.remainingLength += len(b) + } + + if c.Type == PUBLISH { // Fixed flags for PUBLISH packets contain QOS, DUP and RETAIN flags. + p := c.Content.(*Publish) + f := p.QoS << 1 + if p.Duplicate { + f |= 1 << 3 + } + if p.Retain { + f |= 1 + } + c.FixedHeader.Flags = c.Type<<4 | f + } + + var header bytes.Buffer + if _, err := c.FixedHeader.WriteTo(&header); err != nil { + return 0, err + } + + buffers = append(net.Buffers{header.Bytes()}, buffers...) + + if safe, ok := w.(sync.Locker); ok { + safe.Lock() + defer safe.Unlock() + } + return buffers.WriteTo(w) +} + +func encodeVBI(length int) []byte { + var x int + b := [4]byte{} + for { + digit := byte(length % 128) + length /= 128 + if length > 0 { + digit |= 0x80 + } + b[x] = digit + x++ + if length == 0 { + return b[:x] + } + } +} + +func encodeVBIdirect(length int, buf *bytes.Buffer) { + var x int + b := [4]byte{} + for { + digit := byte(length % 128) + length /= 128 + if length > 0 { + digit |= 0x80 + } + b[x] = digit + x++ + if length == 0 { + buf.Write(b[:x]) + return + } + } +} + +func getVBI(r io.Reader) (*bytes.Buffer, error) { + var ret bytes.Buffer + digit := [1]byte{} + for { + _, err := io.ReadFull(r, digit[:]) + if err != nil { + return nil, err + } + ret.WriteByte(digit[0]) + if digit[0] <= 0x7f { + return &ret, nil + } + } +} + +func decodeVBI(r *bytes.Buffer) (int, error) { + var vbi uint32 + var multiplier uint32 + for { + digit, err := r.ReadByte() + if err != nil && err != io.EOF { + return 0, err + } + vbi |= uint32(digit&127) << multiplier + if (digit & 128) == 0 { + break + } + multiplier += 7 + } + return int(vbi), nil +} + +func writeUint16(u uint16, b *bytes.Buffer) error { + if err := b.WriteByte(byte(u >> 8)); err != nil { + return err + } + return b.WriteByte(byte(u)) +} + +func writeUint32(u uint32, b *bytes.Buffer) error { + if err := b.WriteByte(byte(u >> 24)); err != nil { + return err + } + if err := b.WriteByte(byte(u >> 16)); err != nil { + return err + } + if err := b.WriteByte(byte(u >> 8)); err != nil { + return err + } + return b.WriteByte(byte(u)) +} + +func writeString(s string, b *bytes.Buffer) { + writeUint16(uint16(len(s)), b) + b.WriteString(s) +} + +func writeBinary(d []byte, b *bytes.Buffer) { + writeUint16(uint16(len(d)), b) + b.Write(d) +} + +func readUint16(b *bytes.Buffer) (uint16, error) { + b1, err := b.ReadByte() + if err != nil { + return 0, err + } + b2, err := b.ReadByte() + if err != nil { + return 0, err + } + return (uint16(b1) << 8) | uint16(b2), nil +} + +func readUint32(b *bytes.Buffer) (uint32, error) { + b1, err := b.ReadByte() + if err != nil { + return 0, err + } + b2, err := b.ReadByte() + if err != nil { + return 0, err + } + b3, err := b.ReadByte() + if err != nil { + return 0, err + } + b4, err := b.ReadByte() + if err != nil { + return 0, err + } + return (uint32(b1) << 24) | (uint32(b2) << 16) | (uint32(b3) << 8) | uint32(b4), nil +} + +func readBinary(b *bytes.Buffer) ([]byte, error) { + size, err := readUint16(b) + if err != nil { + return nil, err + } + + var s bytes.Buffer + s.Grow(int(size)) + if _, err := io.CopyN(&s, b, int64(size)); err != nil { + return nil, err + } + + return s.Bytes(), nil +} + +func readString(b *bytes.Buffer) (string, error) { + s, err := readBinary(b) + return string(s), err +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/pingreq.go b/vendor/github.com/eclipse/paho.golang/packets/pingreq.go new file mode 100644 index 00000000000..27d39ee324e --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/pingreq.go @@ -0,0 +1,33 @@ +package packets + +import ( + "bytes" + "io" + "net" +) + +// Pingreq is the Variable Header definition for a Pingreq control packet +type Pingreq struct { +} + +func (p *Pingreq) String() string { + return "PINGREQ" +} + +// Unpack is the implementation of the interface required function for a packet +func (p *Pingreq) Unpack(r *bytes.Buffer) error { + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (p *Pingreq) Buffers() net.Buffers { + return nil +} + +// WriteTo is the implementation of the interface required function for a packet +func (p *Pingreq) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: PINGREQ}} + cp.Content = p + + return cp.WriteTo(w) +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/pingresp.go b/vendor/github.com/eclipse/paho.golang/packets/pingresp.go new file mode 100644 index 00000000000..fcf421a763f --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/pingresp.go @@ -0,0 +1,33 @@ +package packets + +import ( + "bytes" + "io" + "net" +) + +// Pingresp is the Variable Header definition for a Pingresp control packet +type Pingresp struct { +} + +func (p *Pingresp) String() string { + return "PINGRESP" +} + +// Unpack is the implementation of the interface required function for a packet +func (p *Pingresp) Unpack(r *bytes.Buffer) error { + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (p *Pingresp) Buffers() net.Buffers { + return nil +} + +// WriteTo is the implementation of the interface required function for a packet +func (p *Pingresp) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: PINGRESP}} + cp.Content = p + + return cp.WriteTo(w) +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/properties.go b/vendor/github.com/eclipse/paho.golang/packets/properties.go new file mode 100644 index 00000000000..5a74e0561a1 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/properties.go @@ -0,0 +1,806 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "strings" +) + +// PropPayloadFormat, etc are the list of property codes for the +// MQTT packet properties +const ( + PropPayloadFormat byte = 1 + PropMessageExpiry byte = 2 + PropContentType byte = 3 + PropResponseTopic byte = 8 + PropCorrelationData byte = 9 + PropSubscriptionIdentifier byte = 11 + PropSessionExpiryInterval byte = 17 + PropAssignedClientID byte = 18 + PropServerKeepAlive byte = 19 + PropAuthMethod byte = 21 + PropAuthData byte = 22 + PropRequestProblemInfo byte = 23 + PropWillDelayInterval byte = 24 + PropRequestResponseInfo byte = 25 + PropResponseInfo byte = 26 + PropServerReference byte = 28 + PropReasonString byte = 31 + PropReceiveMaximum byte = 33 + PropTopicAliasMaximum byte = 34 + PropTopicAlias byte = 35 + PropMaximumQOS byte = 36 + PropRetainAvailable byte = 37 + PropUser byte = 38 + PropMaximumPacketSize byte = 39 + PropWildcardSubAvailable byte = 40 + PropSubIDAvailable byte = 41 + PropSharedSubAvailable byte = 42 +) + +// User is a struct for the User properties, originally it was a map +// then it was pointed out that user properties are allowed to appear +// more than once +type User struct { + Key, Value string +} + +// Properties is a struct representing the all the described properties +// allowed by the MQTT protocol, determining the validity of a property +// relvative to the packettype it was received in is provided by the +// ValidateID function +type Properties struct { + // PayloadFormat indicates the format of the payload of the message + // 0 is unspecified bytes + // 1 is UTF8 encoded character data + PayloadFormat *byte + // MessageExpiry is the lifetime of the message in seconds + MessageExpiry *uint32 + // ContentType is a UTF8 string describing the content of the message + // for example it could be a MIME type + ContentType string + // ResponseTopic is a UTF8 string indicating the topic name to which any + // response to this message should be sent + ResponseTopic string + // CorrelationData is binary data used to associate future response + // messages with the original request message + CorrelationData []byte + // SubscriptionIdentifier is an identifier of the subscription to which + // the Publish matched + SubscriptionIdentifier *int + // SessionExpiryInterval is the time in seconds after a client disconnects + // that the server should retain the session information (subscriptions etc) + SessionExpiryInterval *uint32 + // AssignedClientID is the server assigned client identifier in the case + // that a client connected without specifying a clientID the server + // generates one and returns it in the Connack + AssignedClientID string + // ServerKeepAlive allows the server to specify in the Connack packet + // the time in seconds to be used as the keep alive value + ServerKeepAlive *uint16 + // AuthMethod is a UTF8 string containing the name of the authentication + // method to be used for extended authentication + AuthMethod string + // AuthData is binary data containing authentication data + AuthData []byte + // RequestProblemInfo is used by the Client to indicate to the server to + // include the Reason String and/or User Properties in case of failures + RequestProblemInfo *byte + // WillDelayInterval is the number of seconds the server waits after the + // point at which it would otherwise send the will message before sending + // it. The client reconnecting before that time expires causes the server + // to cancel sending the will + WillDelayInterval *uint32 + // RequestResponseInfo is used by the Client to request the Server provide + // Response Information in the Connack + RequestResponseInfo *byte + // ResponseInfo is a UTF8 encoded string that can be used as the basis for + // createing a Response Topic. The way in which the Client creates a + // Response Topic from the Response Information is not defined. A common + // use of this is to pass a globally unique portion of the topic tree which + // is reserved for this Client for at least the lifetime of its Session. This + // often cannot just be a random name as both the requesting Client and the + // responding Client need to be authorized to use it. It is normal to use this + // as the root of a topic tree for a particular Client. For the Server to + // return this information, it normally needs to be correctly configured. + // Using this mechanism allows this configuration to be done once in the + // Server rather than in each Client + ResponseInfo string + // ServerReference is a UTF8 string indicating another server the client + // can use + ServerReference string + // ReasonString is a UTF8 string representing the reason associated with + // this response, intended to be human readable for diagnostic purposes + ReasonString string + // ReceiveMaximum is the maximum number of QOS1 & 2 messages allowed to be + // 'inflight' (not having received a PUBACK/PUBCOMP response for) + ReceiveMaximum *uint16 + // TopicAliasMaximum is the highest value permitted as a Topic Alias + TopicAliasMaximum *uint16 + // TopicAlias is used in place of the topic string to reduce the size of + // packets for repeated messages on a topic + TopicAlias *uint16 + // MaximumQOS is the highest QOS level permitted for a Publish + MaximumQOS *byte + // RetainAvailable indicates whether the server supports messages with the + // retain flag set + RetainAvailable *byte + // User is a slice of user provided properties (key and value) + User []User + // MaximumPacketSize allows the client or server to specify the maximum packet + // size in bytes that they support + MaximumPacketSize *uint32 + // WildcardSubAvailable indicates whether wildcard subscriptions are permitted + WildcardSubAvailable *byte + // SubIDAvailable indicates whether subscription identifiers are supported + SubIDAvailable *byte + // SharedSubAvailable indicates whether shared subscriptions are supported + SharedSubAvailable *byte +} + +func (p *Properties) String() string { + var b strings.Builder + if p.PayloadFormat != nil { + fmt.Fprintf(&b, "\tPayloadFormat:%d\n", *p.PayloadFormat) + } + if p.MessageExpiry != nil { + fmt.Fprintf(&b, "\tMessageExpiry:%d\n", *p.MessageExpiry) + } + if p.ContentType != "" { + fmt.Fprintf(&b, "\tContentType:%s\n", p.ContentType) + } + if p.ResponseTopic != "" { + fmt.Fprintf(&b, "\tResponseTopic:%s\n", p.ResponseTopic) + } + if len(p.CorrelationData) > 0 { + fmt.Fprintf(&b, "\tCorrelationData:%X\n", p.CorrelationData) + } + if p.SubscriptionIdentifier != nil { + fmt.Fprintf(&b, "\tSubscriptionIdentifier:%d\n", *p.SubscriptionIdentifier) + } + if p.SessionExpiryInterval != nil { + fmt.Fprintf(&b, "\tSessionExpiryInterval:%d\n", *p.SessionExpiryInterval) + } + if p.AssignedClientID != "" { + fmt.Fprintf(&b, "\tAssignedClientID:%s\n", p.AssignedClientID) + } + if p.ServerKeepAlive != nil { + fmt.Fprintf(&b, "\tServerKeepAlive:%d\n", *p.ServerKeepAlive) + } + if p.AuthMethod != "" { + fmt.Fprintf(&b, "\tAuthMethod:%s\n", p.AuthMethod) + } + if len(p.AuthData) > 0 { + fmt.Fprintf(&b, "\tAuthData:%X\n", p.AuthData) + } + if p.RequestProblemInfo != nil { + fmt.Fprintf(&b, "\tRequestProblemInfo:%d\n", *p.RequestProblemInfo) + } + if p.WillDelayInterval != nil { + fmt.Fprintf(&b, "\tWillDelayInterval:%d\n", *p.WillDelayInterval) + } + if p.RequestResponseInfo != nil { + fmt.Fprintf(&b, "\tRequestResponseInfo:%d\n", *p.RequestResponseInfo) + } + if p.ServerReference != "" { + fmt.Fprintf(&b, "\tServerReference:%s\n", p.ServerReference) + } + if p.ReasonString != "" { + fmt.Fprintf(&b, "\tReasonString:%s\n", p.ReasonString) + } + if p.ReceiveMaximum != nil { + fmt.Fprintf(&b, "\tReceiveMaximum:%d\n", *p.ReceiveMaximum) + } + if p.TopicAliasMaximum != nil { + fmt.Fprintf(&b, "\tTopicAliasMaximum:%d\n", *p.TopicAliasMaximum) + } + if p.TopicAlias != nil { + fmt.Fprintf(&b, "\tTopicAlias:%d\n", *p.TopicAlias) + } + if p.MaximumQOS != nil { + fmt.Fprintf(&b, "\tMaximumQOS:%d\n", *p.MaximumQOS) + } + if p.RetainAvailable != nil { + fmt.Fprintf(&b, "\tRetainAvailable:%d\n", *p.RetainAvailable) + } + if p.MaximumPacketSize != nil { + fmt.Fprintf(&b, "\tMaximumPacketSize:%d\n", *p.MaximumPacketSize) + } + if p.WildcardSubAvailable != nil { + fmt.Fprintf(&b, "\tWildcardSubAvailable:%d\n", *p.WildcardSubAvailable) + } + if p.SubIDAvailable != nil { + fmt.Fprintf(&b, "\tSubIDAvailable:%d\n", *p.SubIDAvailable) + } + if p.SharedSubAvailable != nil { + fmt.Fprintf(&b, "\tSharedSubAvailable:%d\n", *p.SharedSubAvailable) + } + if len(p.User) > 0 { + fmt.Fprint(&b, "\tUser Properties:\n") + for _, v := range p.User { + fmt.Fprintf(&b, "\t\t%s:%s\n", v.Key, v.Value) + } + } + + return b.String() +} + +// Pack takes all the defined properties for an Properties and produces +// a slice of bytes representing the wire format for the information +func (i *Properties) Pack(p byte) []byte { + var b bytes.Buffer + + if i == nil { + return nil + } + + if p == PUBLISH { + if i.PayloadFormat != nil { + b.WriteByte(PropPayloadFormat) + b.WriteByte(*i.PayloadFormat) + } + + if i.MessageExpiry != nil { + b.WriteByte(PropMessageExpiry) + writeUint32(*i.MessageExpiry, &b) + } + + if i.ContentType != "" { + b.WriteByte(PropContentType) + writeString(i.ContentType, &b) + } + + if i.ResponseTopic != "" { + b.WriteByte(PropResponseTopic) + writeString(i.ResponseTopic, &b) + } + + if len(i.CorrelationData) > 0 { + b.WriteByte(PropCorrelationData) + writeBinary(i.CorrelationData, &b) + } + + if i.TopicAlias != nil { + b.WriteByte(PropTopicAlias) + writeUint16(*i.TopicAlias, &b) + } + } + + if p == PUBLISH || p == SUBSCRIBE { + if i.SubscriptionIdentifier != nil { + b.WriteByte(PropSubscriptionIdentifier) + encodeVBIdirect(*i.SubscriptionIdentifier, &b) + } + } + + if p == CONNECT || p == CONNACK { + if i.ReceiveMaximum != nil { + b.WriteByte(PropReceiveMaximum) + writeUint16(*i.ReceiveMaximum, &b) + } + + if i.TopicAliasMaximum != nil { + b.WriteByte(PropTopicAliasMaximum) + writeUint16(*i.TopicAliasMaximum, &b) + } + + if i.MaximumQOS != nil { + b.WriteByte(PropMaximumQOS) + b.WriteByte(*i.MaximumQOS) + } + + if i.MaximumPacketSize != nil { + b.WriteByte(PropMaximumPacketSize) + writeUint32(*i.MaximumPacketSize, &b) + } + } + + if p == CONNACK { + if i.AssignedClientID != "" { + b.WriteByte(PropAssignedClientID) + writeString(i.AssignedClientID, &b) + } + + if i.ServerKeepAlive != nil { + b.WriteByte(PropServerKeepAlive) + writeUint16(*i.ServerKeepAlive, &b) + } + + if i.WildcardSubAvailable != nil { + b.WriteByte(PropWildcardSubAvailable) + b.WriteByte(*i.WildcardSubAvailable) + } + + if i.SubIDAvailable != nil { + b.WriteByte(PropSubIDAvailable) + b.WriteByte(*i.SubIDAvailable) + } + + if i.SharedSubAvailable != nil { + b.WriteByte(PropSharedSubAvailable) + b.WriteByte(*i.SharedSubAvailable) + } + + if i.RetainAvailable != nil { + b.WriteByte(PropRetainAvailable) + b.WriteByte(*i.RetainAvailable) + } + + if i.ResponseInfo != "" { + b.WriteByte(PropResponseInfo) + writeString(i.ResponseInfo, &b) + } + } + + if p == CONNECT { + if i.RequestProblemInfo != nil { + b.WriteByte(PropRequestProblemInfo) + b.WriteByte(*i.RequestProblemInfo) + } + + if i.WillDelayInterval != nil { + b.WriteByte(PropWillDelayInterval) + writeUint32(*i.WillDelayInterval, &b) + } + + if i.RequestResponseInfo != nil { + b.WriteByte(PropRequestResponseInfo) + b.WriteByte(*i.RequestResponseInfo) + } + } + + if p == CONNECT || p == CONNACK || p == DISCONNECT { + if i.SessionExpiryInterval != nil { + b.WriteByte(PropSessionExpiryInterval) + writeUint32(*i.SessionExpiryInterval, &b) + } + } + + if p == CONNECT || p == CONNACK || p == AUTH { + if i.AuthMethod != "" { + b.WriteByte(PropAuthMethod) + writeString(i.AuthMethod, &b) + } + + if i.AuthData != nil && len(i.AuthData) > 0 { + b.WriteByte(PropAuthData) + writeBinary(i.AuthData, &b) + } + } + + if p == CONNACK || p == DISCONNECT { + if i.ServerReference != "" { + b.WriteByte(PropServerReference) + writeString(i.ServerReference, &b) + } + } + + if p != CONNECT { + if i.ReasonString != "" { + b.WriteByte(PropReasonString) + writeString(i.ReasonString, &b) + } + } + + for _, v := range i.User { + b.WriteByte(PropUser) + writeString(v.Key, &b) + writeString(v.Value, &b) + } + + return b.Bytes() +} + +// PackBuf will create a bytes.Buffer of the packed properties, it +// will only pack the properties appropriate to the packet type p +// even though other properties may exist, it will silently ignore +// them +func (i *Properties) PackBuf(p byte) *bytes.Buffer { + var b bytes.Buffer + + if i == nil { + return nil + } + + if p == PUBLISH { + if i.PayloadFormat != nil { + b.WriteByte(PropPayloadFormat) + b.WriteByte(*i.PayloadFormat) + } + + if i.MessageExpiry != nil { + b.WriteByte(PropMessageExpiry) + writeUint32(*i.MessageExpiry, &b) + } + + if i.ContentType != "" { + b.WriteByte(PropContentType) + writeString(i.ContentType, &b) + } + + if i.ResponseTopic != "" { + b.WriteByte(PropResponseTopic) + writeString(i.ResponseTopic, &b) + } + + if i.CorrelationData != nil && len(i.CorrelationData) > 0 { + b.WriteByte(PropCorrelationData) + writeBinary(i.CorrelationData, &b) + } + + if i.TopicAlias != nil { + b.WriteByte(PropTopicAlias) + writeUint16(*i.TopicAlias, &b) + } + } + + if p == PUBLISH || p == SUBSCRIBE { + if i.SubscriptionIdentifier != nil { + b.WriteByte(PropSubscriptionIdentifier) + encodeVBIdirect(*i.SubscriptionIdentifier, &b) + } + } + + if p == CONNECT || p == CONNACK { + if i.ReceiveMaximum != nil { + b.WriteByte(PropReceiveMaximum) + writeUint16(*i.ReceiveMaximum, &b) + } + + if i.TopicAliasMaximum != nil { + b.WriteByte(PropTopicAliasMaximum) + writeUint16(*i.TopicAliasMaximum, &b) + } + + if i.MaximumQOS != nil { + b.WriteByte(PropMaximumQOS) + b.WriteByte(*i.MaximumQOS) + } + + if i.MaximumPacketSize != nil { + b.WriteByte(PropMaximumPacketSize) + writeUint32(*i.MaximumPacketSize, &b) + } + } + + if p == CONNACK { + if i.AssignedClientID != "" { + b.WriteByte(PropAssignedClientID) + writeString(i.AssignedClientID, &b) + } + + if i.ServerKeepAlive != nil { + b.WriteByte(PropServerKeepAlive) + writeUint16(*i.ServerKeepAlive, &b) + } + + if i.WildcardSubAvailable != nil { + b.WriteByte(PropWildcardSubAvailable) + b.WriteByte(*i.WildcardSubAvailable) + } + + if i.SubIDAvailable != nil { + b.WriteByte(PropSubIDAvailable) + b.WriteByte(*i.SubIDAvailable) + } + + if i.SharedSubAvailable != nil { + b.WriteByte(PropSharedSubAvailable) + b.WriteByte(*i.SharedSubAvailable) + } + + if i.RetainAvailable != nil { + b.WriteByte(PropRetainAvailable) + b.WriteByte(*i.RetainAvailable) + } + + if i.ResponseInfo != "" { + b.WriteByte(PropResponseInfo) + writeString(i.ResponseInfo, &b) + } + } + + if p == CONNECT { + if i.RequestProblemInfo != nil { + b.WriteByte(PropRequestProblemInfo) + b.WriteByte(*i.RequestProblemInfo) + } + + if i.WillDelayInterval != nil { + b.WriteByte(PropWillDelayInterval) + writeUint32(*i.WillDelayInterval, &b) + } + + if i.RequestResponseInfo != nil { + b.WriteByte(PropRequestResponseInfo) + b.WriteByte(*i.RequestResponseInfo) + } + } + + if p == CONNECT || p == CONNACK || p == DISCONNECT { + if i.SessionExpiryInterval != nil { + b.WriteByte(PropSessionExpiryInterval) + writeUint32(*i.SessionExpiryInterval, &b) + } + } + + if p == CONNECT || p == CONNACK || p == AUTH { + if i.AuthMethod != "" { + b.WriteByte(PropAuthMethod) + writeString(i.AuthMethod, &b) + } + + if i.AuthData != nil && len(i.AuthData) > 0 { + b.WriteByte(PropAuthData) + writeBinary(i.AuthData, &b) + } + } + + if p == CONNACK || p == DISCONNECT { + if i.ServerReference != "" { + b.WriteByte(PropServerReference) + writeString(i.ServerReference, &b) + } + } + + if p != CONNECT { + if i.ReasonString != "" { + b.WriteByte(PropReasonString) + writeString(i.ReasonString, &b) + } + } + + for _, v := range i.User { + b.WriteByte(PropUser) + writeString(v.Key, &b) + writeString(v.Value, &b) + } + + return &b +} + +// Unpack takes a buffer of bytes and reads out the defined properties +// filling in the appropriate entries in the struct, it returns the number +// of bytes used to store the Prop data and any error in decoding them +func (i *Properties) Unpack(r *bytes.Buffer, p byte) error { + vbi, err := getVBI(r) + if err != nil { + return err + } + size, err := decodeVBI(vbi) + if err != nil { + return err + } + if size == 0 { + return nil + } + + buf := bytes.NewBuffer(r.Next(size)) + for { + PropType, err := buf.ReadByte() + if err != nil && err != io.EOF { + return err + } + if err == io.EOF { + break + } + if !ValidateID(p, PropType) { + return fmt.Errorf("invalid Prop type %d for packet %d", PropType, p) + } + switch PropType { + case PropPayloadFormat: + pf, err := buf.ReadByte() + if err != nil { + return err + } + i.PayloadFormat = &pf + case PropMessageExpiry: + pe, err := readUint32(buf) + if err != nil { + return err + } + i.MessageExpiry = &pe + case PropContentType: + ct, err := readString(buf) + if err != nil { + return err + } + i.ContentType = ct + case PropResponseTopic: + tr, err := readString(buf) + if err != nil { + return err + } + i.ResponseTopic = tr + case PropCorrelationData: + cd, err := readBinary(buf) + if err != nil { + return err + } + i.CorrelationData = cd + case PropSubscriptionIdentifier: + si, err := decodeVBI(buf) + if err != nil { + return err + } + i.SubscriptionIdentifier = &si + case PropSessionExpiryInterval: + se, err := readUint32(buf) + if err != nil { + return err + } + i.SessionExpiryInterval = &se + case PropAssignedClientID: + ac, err := readString(buf) + if err != nil { + return err + } + i.AssignedClientID = ac + case PropServerKeepAlive: + sk, err := readUint16(buf) + if err != nil { + return err + } + i.ServerKeepAlive = &sk + case PropAuthMethod: + am, err := readString(buf) + if err != nil { + return err + } + i.AuthMethod = am + case PropAuthData: + ad, err := readBinary(buf) + if err != nil { + return err + } + i.AuthData = ad + case PropRequestProblemInfo: + rp, err := buf.ReadByte() + if err != nil { + return err + } + i.RequestProblemInfo = &rp + case PropWillDelayInterval: + wd, err := readUint32(buf) + if err != nil { + return err + } + i.WillDelayInterval = &wd + case PropRequestResponseInfo: + rp, err := buf.ReadByte() + if err != nil { + return err + } + i.RequestResponseInfo = &rp + case PropResponseInfo: + ri, err := readString(buf) + if err != nil { + return err + } + i.ResponseInfo = ri + case PropServerReference: + sr, err := readString(buf) + if err != nil { + return err + } + i.ServerReference = sr + case PropReasonString: + rs, err := readString(buf) + if err != nil { + return err + } + i.ReasonString = rs + case PropReceiveMaximum: + rm, err := readUint16(buf) + if err != nil { + return err + } + i.ReceiveMaximum = &rm + case PropTopicAliasMaximum: + ta, err := readUint16(buf) + if err != nil { + return err + } + i.TopicAliasMaximum = &ta + case PropTopicAlias: + ta, err := readUint16(buf) + if err != nil { + return err + } + i.TopicAlias = &ta + case PropMaximumQOS: + mq, err := buf.ReadByte() + if err != nil { + return err + } + i.MaximumQOS = &mq + case PropRetainAvailable: + ra, err := buf.ReadByte() + if err != nil { + return err + } + i.RetainAvailable = &ra + case PropUser: + k, err := readString(buf) + if err != nil { + return err + } + v, err := readString(buf) + if err != nil { + return err + } + i.User = append(i.User, User{k, v}) + case PropMaximumPacketSize: + mp, err := readUint32(buf) + if err != nil { + return err + } + i.MaximumPacketSize = &mp + case PropWildcardSubAvailable: + ws, err := buf.ReadByte() + if err != nil { + return err + } + i.WildcardSubAvailable = &ws + case PropSubIDAvailable: + si, err := buf.ReadByte() + if err != nil { + return err + } + i.SubIDAvailable = &si + case PropSharedSubAvailable: + ss, err := buf.ReadByte() + if err != nil { + return err + } + i.SharedSubAvailable = &ss + default: + return fmt.Errorf("unknown Prop type %d", PropType) + } + } + + return nil +} + +// ValidProperties is a map of the various properties and the +// PacketTypes that property is valid for. +// A CONNECT packet has own properties, but may also include a separate set of Will Properties. +// Currently, `CONNECT` covers both sets, this may lead to some invalid properties being accepted (this may be fixed in the future). +var ValidProperties = map[byte]map[byte]struct{}{ + PropPayloadFormat: {CONNECT: {}, PUBLISH: {}}, + PropMessageExpiry: {CONNECT: {}, PUBLISH: {}}, + PropContentType: {CONNECT: {}, PUBLISH: {}}, + PropResponseTopic: {CONNECT: {}, PUBLISH: {}}, + PropCorrelationData: {CONNECT: {}, PUBLISH: {}}, + PropTopicAlias: {PUBLISH: {}}, + PropSubscriptionIdentifier: {PUBLISH: {}, SUBSCRIBE: {}}, + PropSessionExpiryInterval: {CONNECT: {}, CONNACK: {}, DISCONNECT: {}}, + PropAssignedClientID: {CONNACK: {}}, + PropServerKeepAlive: {CONNACK: {}}, + PropWildcardSubAvailable: {CONNACK: {}}, + PropSubIDAvailable: {CONNACK: {}}, + PropSharedSubAvailable: {CONNACK: {}}, + PropRetainAvailable: {CONNACK: {}}, + PropResponseInfo: {CONNACK: {}}, + PropAuthMethod: {CONNECT: {}, CONNACK: {}, AUTH: {}}, + PropAuthData: {CONNECT: {}, CONNACK: {}, AUTH: {}}, + PropRequestProblemInfo: {CONNECT: {}}, + PropWillDelayInterval: {CONNECT: {}}, + PropRequestResponseInfo: {CONNECT: {}}, + PropServerReference: {CONNACK: {}, DISCONNECT: {}}, + PropReasonString: {CONNACK: {}, PUBACK: {}, PUBREC: {}, PUBREL: {}, PUBCOMP: {}, SUBACK: {}, UNSUBACK: {}, DISCONNECT: {}, AUTH: {}}, + PropReceiveMaximum: {CONNECT: {}, CONNACK: {}}, + PropTopicAliasMaximum: {CONNECT: {}, CONNACK: {}}, + PropMaximumQOS: {CONNECT: {}, CONNACK: {}}, + PropMaximumPacketSize: {CONNECT: {}, CONNACK: {}}, + PropUser: {CONNECT: {}, CONNACK: {}, PUBLISH: {}, PUBACK: {}, PUBREC: {}, PUBREL: {}, PUBCOMP: {}, SUBSCRIBE: {}, UNSUBSCRIBE: {}, SUBACK: {}, UNSUBACK: {}, DISCONNECT: {}, AUTH: {}}, +} + +// ValidateID takes a PacketType and a property name and returns +// a boolean indicating if that property is valid for that +// PacketType +func ValidateID(p byte, i byte) bool { + _, ok := ValidProperties[i][p] + return ok +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/puback.go b/vendor/github.com/eclipse/paho.golang/packets/puback.go new file mode 100644 index 00000000000..67f404ce6a6 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/puback.go @@ -0,0 +1,115 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" +) + +// Puback is the Variable Header definition for a Puback control packet +type Puback struct { + Properties *Properties + PacketID uint16 + ReasonCode byte +} + +// PubackSuccess, etc are the list of valid puback reason codes. +const ( + PubackSuccess = 0x00 + PubackNoMatchingSubscribers = 0x10 + PubackUnspecifiedError = 0x80 + PubackImplementationSpecificError = 0x83 + PubackNotAuthorized = 0x87 + PubackTopicNameInvalid = 0x90 + PubackPacketIdentifierInUse = 0x91 + PubackQuotaExceeded = 0x97 + PubackPayloadFormatInvalid = 0x99 +) + +func (p *Puback) String() string { + var b strings.Builder + + fmt.Fprintf(&b, "PUBACK: PacketID:%d ReasonCode:%X", p.PacketID, p.ReasonCode) + if p.Properties != nil { + fmt.Fprintf(&b, " Properties:\n%s", p.Properties) + } else { + fmt.Fprint(&b, "\n") + } + + return b.String() +} + +//Unpack is the implementation of the interface required function for a packet +func (p *Puback) Unpack(r *bytes.Buffer) error { + var err error + success := r.Len() == 2 + noProps := r.Len() == 3 + p.PacketID, err = readUint16(r) + if err != nil { + return err + } + if !success { + p.ReasonCode, err = r.ReadByte() + if err != nil { + return err + } + + if !noProps { + err = p.Properties.Unpack(r, PUBACK) + if err != nil { + return err + } + } + } + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (p *Puback) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(p.PacketID, &b) + b.WriteByte(p.ReasonCode) + idvp := p.Properties.Pack(PUBACK) + propLen := encodeVBI(len(idvp)) + n := net.Buffers{b.Bytes(), propLen} + if len(idvp) > 0 { + n = append(n, idvp) + } + return n +} + +// WriteTo is the implementation of the interface required function for a packet +func (p *Puback) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBACK}} + cp.Content = p + + return cp.WriteTo(w) +} + +// Reason returns a string representation of the meaning of the ReasonCode +func (p *Puback) Reason() string { + switch p.ReasonCode { + case 0: + return "The message is accepted. Publication of the QoS 1 message proceeds." + case 16: + 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)." + case 128: + 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." + case 131: + return "The PUBLISH is valid but the receiver is not willing to accept it." + case 135: + return "The PUBLISH is not authorized." + case 144: + return "The Topic Name is not malformed, but is not accepted by this Client or Server." + case 145: + return "The Packet Identifier is already in use. This might indicate a mismatch in the Session State between the Client and Server." + case 151: + return "An implementation or administrative imposed limit has been exceeded." + case 153: + return "The payload format does not match the specified Payload Format Indicator." + } + + return "" +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/pubcomp.go b/vendor/github.com/eclipse/paho.golang/packets/pubcomp.go new file mode 100644 index 00000000000..1cdfe61e98b --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/pubcomp.go @@ -0,0 +1,95 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" +) + +// Pubcomp is the Variable Header definition for a Pubcomp control packet +type Pubcomp struct { + Properties *Properties + PacketID uint16 + ReasonCode byte +} + +// PubcompSuccess, etc are the list of valid pubcomp reason codes. +const ( + PubcompSuccess = 0x00 + PubcompPacketIdentifierNotFound = 0x92 +) + +func (p *Pubcomp) String() string { + var b strings.Builder + + fmt.Fprintf(&b, "PUBCOMP: ReasonCode:%X PacketID:%d", p.ReasonCode, p.PacketID) + if p.Properties != nil { + fmt.Fprintf(&b, " Properties:\n%s", p.Properties) + } else { + fmt.Fprint(&b, "\n") + } + + return b.String() +} + +//Unpack is the implementation of the interface required function for a packet +func (p *Pubcomp) Unpack(r *bytes.Buffer) error { + var err error + success := r.Len() == 2 + noProps := r.Len() == 3 + p.PacketID, err = readUint16(r) + if err != nil { + return err + } + if !success { + p.ReasonCode, err = r.ReadByte() + if err != nil { + return err + } + + if !noProps { + err = p.Properties.Unpack(r, PUBACK) + if err != nil { + return err + } + } + } + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (p *Pubcomp) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(p.PacketID, &b) + b.WriteByte(p.ReasonCode) + n := net.Buffers{b.Bytes()} + idvp := p.Properties.Pack(PUBCOMP) + propLen := encodeVBI(len(idvp)) + if len(idvp) > 0 { + n = append(n, propLen) + n = append(n, idvp) + } + return n +} + +// WriteTo is the implementation of the interface required function for a packet +func (p *Pubcomp) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBCOMP}} + cp.Content = p + + return cp.WriteTo(w) +} + +// Reason returns a string representation of the meaning of the ReasonCode +func (p *Pubcomp) Reason() string { + switch p.ReasonCode { + case 0: + return "Success - Packet Identifier released. Publication of QoS 2 message is complete." + case 146: + 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." + } + + return "" +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/publish.go b/vendor/github.com/eclipse/paho.golang/packets/publish.go new file mode 100644 index 00000000000..24edb588b23 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/publish.go @@ -0,0 +1,92 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net" +) + +// Publish is the Variable Header definition for a publish control packet +type Publish struct { + Payload []byte + Topic string + Properties *Properties + PacketID uint16 + QoS byte + Duplicate bool + Retain bool +} + +func (p *Publish) String() string { + return fmt.Sprintf("PUBLISH: PacketID:%d QOS:%d Topic:%s Duplicate:%t Retain:%t Payload:\n%s\nProperties\n%s", p.PacketID, p.QoS, p.Topic, p.Duplicate, p.Retain, string(p.Payload), p.Properties) +} + +// SetIdentifier sets the packet identifier +func (p *Publish) SetIdentifier(packetID uint16) { + p.PacketID = packetID +} + +// Type returns the current packet type +func (s *Publish) Type() byte { + return PUBLISH +} + +// Unpack is the implementation of the interface required function for a packet +func (p *Publish) Unpack(r *bytes.Buffer) error { + var err error + p.Topic, err = readString(r) + if err != nil { + return err + } + if p.QoS > 0 { + p.PacketID, err = readUint16(r) + if err != nil { + return err + } + } + + err = p.Properties.Unpack(r, PUBLISH) + if err != nil { + return err + } + + p.Payload, err = ioutil.ReadAll(r) + if err != nil { + return err + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (p *Publish) Buffers() net.Buffers { + var b bytes.Buffer + writeString(p.Topic, &b) + if p.QoS > 0 { + _ = writeUint16(p.PacketID, &b) + } + idvp := p.Properties.Pack(PUBLISH) + encodeVBIdirect(len(idvp), &b) + return net.Buffers{b.Bytes(), idvp, p.Payload} + +} + +// WriteTo is the implementation of the interface required function for a packet +func (p *Publish) WriteTo(w io.Writer) (int64, error) { + return p.ToControlPacket().WriteTo(w) +} + +// ToControlPacket returns the packet as a ControlPacket +func (p *Publish) ToControlPacket() *ControlPacket { + f := p.QoS << 1 + if p.Duplicate { + f |= 1 << 3 + } + if p.Retain { + f |= 1 + } + + return &ControlPacket{FixedHeader: FixedHeader{Type: PUBLISH, Flags: f}, Content: p} +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/pubrec.go b/vendor/github.com/eclipse/paho.golang/packets/pubrec.go new file mode 100644 index 00000000000..7bd30451099 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/pubrec.go @@ -0,0 +1,119 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" +) + +// Pubrec is the Variable Header definition for a Pubrec control packet +type Pubrec struct { + Properties *Properties + PacketID uint16 + ReasonCode byte +} + +// PubrecSuccess, etc are the list of valid Pubrec reason codes +const ( + PubrecSuccess = 0x00 + PubrecNoMatchingSubscribers = 0x10 + PubrecUnspecifiedError = 0x80 + PubrecImplementationSpecificError = 0x83 + PubrecNotAuthorized = 0x87 + PubrecTopicNameInvalid = 0x90 + PubrecPacketIdentifierInUse = 0x91 + PubrecQuotaExceeded = 0x97 + PubrecPayloadFormatInvalid = 0x99 +) + +func (p *Pubrec) String() string { + var b strings.Builder + + fmt.Fprintf(&b, "PUBREC: ReasonCode:%X PacketID:%d", p.ReasonCode, p.PacketID) + if p.Properties != nil { + fmt.Fprintf(&b, " Properties:\n%s", p.Properties) + } else { + fmt.Fprint(&b, "\n") + } + + return b.String() +} + +// Unpack is the implementation of the interface required function for a packet +func (p *Pubrec) Unpack(r *bytes.Buffer) error { + var err error + success := r.Len() == 2 + noProps := r.Len() == 3 + p.PacketID, err = readUint16(r) + if err != nil { + return err + } + if !success { + p.ReasonCode, err = r.ReadByte() + if err != nil { + return err + } + + if !noProps { + err = p.Properties.Unpack(r, PUBACK) + if err != nil { + return err + } + } + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (p *Pubrec) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(p.PacketID, &b) + b.WriteByte(p.ReasonCode) + n := net.Buffers{b.Bytes()} + idvp := p.Properties.Pack(PUBREC) + propLen := encodeVBI(len(idvp)) + if len(idvp) > 0 { + n = append(n, propLen) + n = append(n, idvp) + } + return n +} + +// WriteTo is the implementation of the interface required function for a packet +func (p *Pubrec) WriteTo(w io.Writer) (int64, error) { + return p.ToControlPacket().WriteTo(w) +} + +// ToControlPacket returns the packet as a ControlPacket +func (p *Pubrec) ToControlPacket() *ControlPacket { + return &ControlPacket{FixedHeader: FixedHeader{Type: PUBREC}, Content: p} +} + +// Reason returns a string representation of the meaning of the ReasonCode +func (p *Pubrec) Reason() string { + switch p.ReasonCode { + case 0: + return "Success - The message is accepted. Publication of the QoS 2 message proceeds." + case 16: + 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)" + case 128: + 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." + case 131: + return "Implementation specific error - The PUBLISH is valid but the receiver is not willing to accept it." + case 135: + return "Not authorized - The PUBLISH is not authorized." + case 144: + return "Topic Name invalid - The Topic Name is not malformed, but is not accepted by this Client or Server." + case 145: + 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." + case 151: + return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." + case 153: + return "Payload format invalid - The payload format does not match the one specified in the Payload Format Indicator." + } + + return "" +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/pubrel.go b/vendor/github.com/eclipse/paho.golang/packets/pubrel.go new file mode 100644 index 00000000000..27c48c24042 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/pubrel.go @@ -0,0 +1,77 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" +) + +// Pubrel is the Variable Header definition for a Pubrel control packet +type Pubrel struct { + Properties *Properties + PacketID uint16 + ReasonCode byte +} + +func (p *Pubrel) String() string { + var b strings.Builder + + fmt.Fprintf(&b, "PUBREL: ReasonCode:%X PacketID:%d", p.ReasonCode, p.PacketID) + if p.Properties != nil { + fmt.Fprintf(&b, " Properties:\n%s", p.Properties) + } else { + fmt.Fprint(&b, "\n") + } + + return b.String() +} + +//Unpack is the implementation of the interface required function for a packet +func (p *Pubrel) Unpack(r *bytes.Buffer) error { + var err error + success := r.Len() == 2 + noProps := r.Len() == 3 + p.PacketID, err = readUint16(r) + if err != nil { + return err + } + if !success { + p.ReasonCode, err = r.ReadByte() + if err != nil { + return err + } + + if !noProps { + err = p.Properties.Unpack(r, PUBACK) + if err != nil { + return err + } + } + } + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (p *Pubrel) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(p.PacketID, &b) + b.WriteByte(p.ReasonCode) + n := net.Buffers{b.Bytes()} + idvp := p.Properties.Pack(PUBREL) + propLen := encodeVBI(len(idvp)) + if len(idvp) > 0 { + n = append(n, propLen) + n = append(n, idvp) + } + return n +} + +// WriteTo is the implementation of the interface required function for a packet +func (p *Pubrel) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: PUBREL, Flags: 2}} + cp.Content = p + + return cp.WriteTo(w) +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/suback.go b/vendor/github.com/eclipse/paho.golang/packets/suback.go new file mode 100644 index 00000000000..2503aaf1aec --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/suback.go @@ -0,0 +1,103 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" +) + +// Suback is the Variable Header definition for a Suback control packet +type Suback struct { + Properties *Properties + Reasons []byte + PacketID uint16 +} + +func (s *Suback) String() string { + return fmt.Sprintf("SUBACK: ReasonCode:%v PacketID:%d Properties:\n%s", s.Reasons, s.PacketID, s.Properties) +} + +// SubackGrantedQoS0, etc are the list of valid suback reason codes. +const ( + SubackGrantedQoS0 = 0x00 + SubackGrantedQoS1 = 0x01 + SubackGrantedQoS2 = 0x02 + SubackUnspecifiederror = 0x80 + SubackImplementationspecificerror = 0x83 + SubackNotauthorized = 0x87 + SubackTopicFilterinvalid = 0x8F + SubackPacketIdentifierinuse = 0x91 + SubackQuotaexceeded = 0x97 + SubackSharedSubscriptionnotsupported = 0x9E + SubackSubscriptionIdentifiersnotsupported = 0xA1 + SubackWildcardsubscriptionsnotsupported = 0xA2 +) + +//Unpack is the implementation of the interface required function for a packet +func (s *Suback) Unpack(r *bytes.Buffer) error { + var err error + s.PacketID, err = readUint16(r) + if err != nil { + return err + } + + err = s.Properties.Unpack(r, SUBACK) + if err != nil { + return err + } + + s.Reasons = r.Bytes() + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (s *Suback) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(s.PacketID, &b) + idvp := s.Properties.Pack(SUBACK) + propLen := encodeVBI(len(idvp)) + return net.Buffers{b.Bytes(), propLen, idvp, s.Reasons} +} + +// WriteTo is the implementation of the interface required function for a packet +func (s *Suback) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: SUBACK}} + cp.Content = s + + return cp.WriteTo(w) +} + +// Reason returns a string representation of the meaning of the ReasonCode +func (s *Suback) Reason(index int) string { + if index >= 0 && index < len(s.Reasons) { + switch s.Reasons[index] { + case 0: + 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." + case 1: + 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." + case 2: + return "Granted QoS 2 - The subscription is accepted and any received QoS will be sent to this subscription." + case 128: + 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." + case 131: + return "Implementation specific error - The SUBSCRIBE is valid but the Server does not accept it." + case 135: + return "Not authorized - The Client is not authorized to make this subscription." + case 143: + return "Topic Filter invalid - The Topic Filter is correctly formed but is not allowed for this Client." + case 145: + return "Packet Identifier in use - The specified Packet Identifier is already in use." + case 151: + return "Quota exceeded - An implementation or administrative imposed limit has been exceeded." + case 158: + return "Shared Subscription not supported - The Server does not support Shared Subscriptions for this Client." + case 161: + return "Subscription Identifiers not supported - The Server does not support Subscription Identifiers; the subscription is not accepted." + case 162: + return "Wildcard subscriptions not supported - The Server does not support Wildcard subscription; the subscription is not accepted." + } + } + return "Invalid Reason index" +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/subscribe.go b/vendor/github.com/eclipse/paho.golang/packets/subscribe.go new file mode 100644 index 00000000000..2abccbbd674 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/subscribe.go @@ -0,0 +1,130 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" +) + +// Subscribe is the Variable Header definition for a Subscribe control packet +type Subscribe struct { + Properties *Properties + Subscriptions []SubOptions + PacketID uint16 +} + +func (s *Subscribe) String() string { + var b strings.Builder + + fmt.Fprintf(&b, "SUBSCRIBE: PacketID:%d Subscriptions:\n", s.PacketID) + for _, o := range s.Subscriptions { + fmt.Fprintf(&b, "\t%s: QOS:%d RetainHandling:%X NoLocal:%t RetainAsPublished:%t\n", o.Topic, o.QoS, o.RetainHandling, o.NoLocal, o.RetainAsPublished) + } + fmt.Fprintf(&b, "Properties:\n%s", s.Properties) + + return b.String() +} + +// SetIdentifier sets the packet identifier +func (s *Subscribe) SetIdentifier(packetID uint16) { + s.PacketID = packetID +} + +// Type returns the current packet type +func (s *Subscribe) Type() byte { + return SUBSCRIBE +} + +// SubOptions is the struct representing the options for a subscription +type SubOptions struct { + Topic string + QoS byte + RetainHandling byte + NoLocal bool + RetainAsPublished bool +} + +// Pack is the implementation of the interface required function for a packet +// Note that this does not pack the topic +func (s *SubOptions) Pack() byte { + var ret byte + ret |= s.QoS & 0x03 + if s.NoLocal { + ret |= 1 << 2 + } + if s.RetainAsPublished { + ret |= 1 << 3 + } + ret |= (s.RetainHandling << 4) & 0x30 + + return ret +} + +// Unpack is the implementation of the interface required function for a packet +// Note that this does not unpack the topic +func (s *SubOptions) Unpack(r *bytes.Buffer) error { + b, err := r.ReadByte() + if err != nil { + return err + } + + s.QoS = b & 0x03 + s.NoLocal = b&(1<<2) != 0 + s.RetainAsPublished = b&(1<<3) != 0 + s.RetainHandling = 3 & (b >> 4) + + return nil +} + +// Unpack is the implementation of the interface required function for a packet +func (s *Subscribe) Unpack(r *bytes.Buffer) error { + var err error + s.PacketID, err = readUint16(r) + if err != nil { + return err + } + + err = s.Properties.Unpack(r, SUBSCRIBE) + if err != nil { + return err + } + + for r.Len() > 0 { + var so SubOptions + t, err := readString(r) + if err != nil { + return err + } + if err = so.Unpack(r); err != nil { + return err + } + so.Topic = t + s.Subscriptions = append(s.Subscriptions, so) + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (s *Subscribe) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(s.PacketID, &b) + var subs bytes.Buffer + for _, o := range s.Subscriptions { + writeString(o.Topic, &subs) + subs.WriteByte(o.Pack()) + } + idvp := s.Properties.Pack(SUBSCRIBE) + propLen := encodeVBI(len(idvp)) + return net.Buffers{b.Bytes(), propLen, idvp, subs.Bytes()} +} + +// WriteTo is the implementation of the interface required function for a packet +func (s *Subscribe) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: SUBSCRIBE, Flags: 2}} + cp.Content = s + + return cp.WriteTo(w) +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/unsuback.go b/vendor/github.com/eclipse/paho.golang/packets/unsuback.go new file mode 100644 index 00000000000..ba5164b9f19 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/unsuback.go @@ -0,0 +1,88 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" +) + +// Unsuback is the Variable Header definition for a Unsuback control packet +type Unsuback struct { + Reasons []byte + Properties *Properties + PacketID uint16 +} + +func (u *Unsuback) String() string { + return fmt.Sprintf("UNSUBACK: ReasonCode:%v PacketID:%d Properties:\n%s", u.Reasons, u.PacketID, u.Properties) +} + +// UnsubackSuccess, etc are the list of valid unsuback reason codes. +const ( + UnsubackSuccess = 0x00 + UnsubackNoSubscriptionFound = 0x11 + UnsubackUnspecifiedError = 0x80 + UnsubackImplementationSpecificError = 0x83 + UnsubackNotAuthorized = 0x87 + UnsubackTopicFilterInvalid = 0x8F + UnsubackPacketIdentifierInUse = 0x91 +) + +// Unpack is the implementation of the interface required function for a packet +func (u *Unsuback) Unpack(r *bytes.Buffer) error { + var err error + u.PacketID, err = readUint16(r) + if err != nil { + return err + } + + err = u.Properties.Unpack(r, UNSUBACK) + if err != nil { + return err + } + + u.Reasons = r.Bytes() + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (u *Unsuback) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(u.PacketID, &b) + idvp := u.Properties.Pack(UNSUBACK) + propLen := encodeVBI(len(idvp)) + return net.Buffers{b.Bytes(), propLen, idvp, u.Reasons} +} + +// WriteTo is the implementation of the interface required function for a packet +func (u *Unsuback) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: UNSUBACK}} + cp.Content = u + + return cp.WriteTo(w) +} + +// Reason returns a string representation of the meaning of the ReasonCode +func (u *Unsuback) Reason(index int) string { + if index >= 0 && index < len(u.Reasons) { + switch u.Reasons[index] { + case 0x00: + return "Success - The subscription is deleted" + case 0x11: + return "No subscription found - No matching Topic Filter is being used by the Client." + case 0x80: + 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." + case 0x83: + return "Implementation specific error - The UNSUBSCRIBE is valid but the Server does not accept it." + case 0x87: + return "Not authorized - The Client is not authorized to unsubscribe." + case 0x8F: + return "Topic Filter invalid - The Topic Filter is correctly formed but is not allowed for this Client." + case 0x91: + return "Packet Identifier in use - The specified Packet Identifier is already in use." + } + } + return "Invalid Reason index" +} diff --git a/vendor/github.com/eclipse/paho.golang/packets/unsubscribe.go b/vendor/github.com/eclipse/paho.golang/packets/unsubscribe.go new file mode 100644 index 00000000000..090d7ca7209 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/packets/unsubscribe.go @@ -0,0 +1,77 @@ +package packets + +import ( + "bytes" + "fmt" + "io" + "net" +) + +// Unsubscribe is the Variable Header definition for a Unsubscribe control packet +type Unsubscribe struct { + Topics []string + Properties *Properties + PacketID uint16 +} + +func (u *Unsubscribe) String() string { + return fmt.Sprintf("UNSUBSCRIBE: PacketID:%d Topics:%v Properties:\n%s", u.PacketID, u.Topics, u.Properties) +} + +// SetIdentifier sets the packet identifier +func (u *Unsubscribe) SetIdentifier(packetID uint16) { + u.PacketID = packetID +} + +// Type returns the current packet type +func (s *Unsubscribe) Type() byte { + return UNSUBSCRIBE +} + +// Unpack is the implementation of the interface required function for a packet +func (u *Unsubscribe) Unpack(r *bytes.Buffer) error { + var err error + u.PacketID, err = readUint16(r) + if err != nil { + return err + } + + err = u.Properties.Unpack(r, UNSUBSCRIBE) + if err != nil { + return err + } + + for { + t, err := readString(r) + if err != nil && err != io.EOF { + return err + } + if err == io.EOF { + break + } + u.Topics = append(u.Topics, t) + } + + return nil +} + +// Buffers is the implementation of the interface required function for a packet +func (u *Unsubscribe) Buffers() net.Buffers { + var b bytes.Buffer + writeUint16(u.PacketID, &b) + var topics bytes.Buffer + for _, t := range u.Topics { + writeString(t, &topics) + } + idvp := u.Properties.Pack(UNSUBSCRIBE) + propLen := encodeVBI(len(idvp)) + return net.Buffers{b.Bytes(), propLen, idvp, topics.Bytes()} +} + +// WriteTo is the implementation of the interface required function for a packet +func (u *Unsubscribe) WriteTo(w io.Writer) (int64, error) { + cp := &ControlPacket{FixedHeader: FixedHeader{Type: UNSUBSCRIBE, Flags: 2}} + cp.Content = u + + return cp.WriteTo(w) +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/acks_tracker.go b/vendor/github.com/eclipse/paho.golang/paho/acks_tracker.go new file mode 100644 index 00000000000..47f11cb67f4 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/acks_tracker.go @@ -0,0 +1,79 @@ +package paho + +import ( + "errors" + "sync" + + "github.com/eclipse/paho.golang/packets" +) + +var ( + ErrPacketNotFound = errors.New("packet not found") +) + +type acksTracker struct { + mx sync.Mutex + order []packet +} + +func (t *acksTracker) add(pb *packets.Publish) { + t.mx.Lock() + defer t.mx.Unlock() + + for _, v := range t.order { + if v.pb.PacketID == pb.PacketID { + return // already added + } + } + + t.order = append(t.order, packet{pb: pb}) +} + +func (t *acksTracker) markAsAcked(pb *packets.Publish) error { + t.mx.Lock() + defer t.mx.Unlock() + + for k, v := range t.order { + if pb.PacketID == v.pb.PacketID { + t.order[k].acknowledged = true + return nil + } + } + + return ErrPacketNotFound +} + +func (t *acksTracker) flush(do func([]*packets.Publish)) { + t.mx.Lock() + defer t.mx.Unlock() + + var ( + buf []*packets.Publish + ) + for _, v := range t.order { + if v.acknowledged { + buf = append(buf, v.pb) + } else { + break + } + } + + if len(buf) == 0 { + return + } + + do(buf) + t.order = t.order[len(buf):] +} + +// reset should be used upon disconnections +func (t *acksTracker) reset() { + t.mx.Lock() + defer t.mx.Unlock() + t.order = nil +} + +type packet struct { + pb *packets.Publish + acknowledged bool +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/auth.go b/vendor/github.com/eclipse/paho.golang/paho/auth.go new file mode 100644 index 00000000000..7d3a3c97233 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/auth.go @@ -0,0 +1,8 @@ +package paho + +// Auther is the interface for something that implements the extended authentication +// flows in MQTT v5 +type Auther interface { + Authenticate(*Auth) *Auth + Authenticated() +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/client.go b/vendor/github.com/eclipse/paho.golang/paho/client.go new file mode 100644 index 00000000000..76a98722933 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/client.go @@ -0,0 +1,927 @@ +package paho + +import ( + "context" + "errors" + "fmt" + "math" + "net" + "strings" + "sync" + "time" + + "github.com/eclipse/paho.golang/packets" + "golang.org/x/sync/semaphore" +) + +type MQTTVersion byte + +const ( + MQTTv311 MQTTVersion = 4 + MQTTv5 MQTTVersion = 5 +) + +const defaultSendAckInterval = 50 * time.Millisecond + +var ( + ErrManualAcknowledgmentDisabled = errors.New("manual acknowledgments disabled") +) + +type ( + // ClientConfig are the user configurable options for the client, an + // instance of this struct is passed into NewClient(), not all options + // are required to be set, defaults are provided for Persistence, MIDs, + // PingHandler, PacketTimeout and Router. + ClientConfig struct { + ClientID string + // Conn is the connection to broker. + // BEWARE that most wrapped net.Conn implementations like tls.Conn are + // not thread safe for writing. To fix, use packets.NewThreadSafeConn + // wrapper or extend the custom net.Conn struct with sync.Locker. + Conn net.Conn + MIDs MIDService + AuthHandler Auther + PingHandler Pinger + Router Router + Persistence Persistence + PacketTimeout time.Duration + // OnServerDisconnect is called only when a packets.DISCONNECT is received from server + OnServerDisconnect func(*Disconnect) + // OnClientError is for example called on net.Error + OnClientError func(error) + // PublishHook allows a user provided function to be called before + // a Publish packet is sent allowing it to inspect or modify the + // Publish, an example of the utility of this is provided in the + // Topic Alias Handler extension which will automatically assign + // and use topic alias values rather than topic strings. + PublishHook func(*Publish) + // EnableManualAcknowledgment is used to control the acknowledgment of packets manually. + // BEWARE that the MQTT specs require clients to send acknowledgments in the order in which the corresponding + // PUBLISH packets were received. + // Consider the following scenario: the client receives packets 1,2,3,4 + // If you acknowledge 3 first, no ack is actually sent to the server but it's buffered until also 1 and 2 + // are acknowledged. + EnableManualAcknowledgment bool + // SendAcksInterval is used only when EnableManualAcknowledgment is true + // it determines how often the client tries to send a batch of acknowledgments in the right order to the server. + SendAcksInterval time.Duration + } + // Client is the struct representing an MQTT client + Client struct { + mu sync.Mutex + ClientConfig + // raCtx is used for handling the MQTTv5 authentication exchange. + raCtx *CPContext + stop chan struct{} + publishPackets chan *packets.Publish + acksTracker acksTracker + workers sync.WaitGroup + serverProps CommsProperties + clientProps CommsProperties + serverInflight *semaphore.Weighted + clientInflight *semaphore.Weighted + debug Logger + errors Logger + } + + // CommsProperties is a struct of the communication properties that may + // be set by the server in the Connack and that the client needs to be + // aware of for future subscribes/publishes + CommsProperties struct { + MaximumPacketSize uint32 + ReceiveMaximum uint16 + TopicAliasMaximum uint16 + MaximumQoS byte + RetainAvailable bool + WildcardSubAvailable bool + SubIDAvailable bool + SharedSubAvailable bool + } + + caContext struct { + Context context.Context + Return chan *packets.Connack + } +) + +// NewClient is used to create a new default instance of an MQTT client. +// It returns a pointer to the new client instance. +// The default client uses the provided PingHandler, MessageID and +// StandardRouter implementations, and a noop Persistence. +// These should be replaced if desired before the client is connected. +// client.Conn *MUST* be set to an already connected net.Conn before +// Connect() is called. +func NewClient(conf ClientConfig) *Client { + c := &Client{ + serverProps: CommsProperties{ + ReceiveMaximum: 65535, + MaximumQoS: 2, + MaximumPacketSize: 0, + TopicAliasMaximum: 0, + RetainAvailable: true, + WildcardSubAvailable: true, + SubIDAvailable: true, + SharedSubAvailable: true, + }, + clientProps: CommsProperties{ + ReceiveMaximum: 65535, + MaximumQoS: 2, + MaximumPacketSize: 0, + TopicAliasMaximum: 0, + }, + ClientConfig: conf, + errors: NOOPLogger{}, + debug: NOOPLogger{}, + } + + if c.Persistence == nil { + c.Persistence = &noopPersistence{} + } + if c.MIDs == nil { + c.MIDs = &MIDs{index: make([]*CPContext, int(midMax))} + } + if c.PacketTimeout == 0 { + c.PacketTimeout = 10 * time.Second + } + if c.Router == nil { + c.Router = NewStandardRouter() + } + if c.PingHandler == nil { + c.PingHandler = DefaultPingerWithCustomFailHandler(func(e error) { + go c.error(e) + }) + } + if c.OnClientError == nil { + c.OnClientError = func(e error) {} + } + + return c +} + +// Connect is used to connect the client to a server. It presumes that +// the Client instance already has a working network connection. +// The function takes a pre-prepared Connect packet, and uses that to +// establish an MQTT connection. Assuming the connection completes +// successfully the rest of the client is initiated and the Connack +// returned. Otherwise the failure Connack (if there is one) is returned +// along with an error indicating the reason for the failure to connect. +func (c *Client) Connect(ctx context.Context, cp *Connect) (*Connack, error) { + if c.Conn == nil { + return nil, fmt.Errorf("client connection is nil") + } + + cleanup := func() { + close(c.stop) + close(c.publishPackets) + _ = c.Conn.Close() + c.mu.Unlock() + } + + c.mu.Lock() + c.stop = make(chan struct{}) + + var publishPacketsSize uint16 = math.MaxUint16 + if cp.Properties != nil && cp.Properties.ReceiveMaximum != nil { + publishPacketsSize = *cp.Properties.ReceiveMaximum + } + c.publishPackets = make(chan *packets.Publish, publishPacketsSize) + + keepalive := cp.KeepAlive + c.ClientID = cp.ClientID + if cp.Properties != nil { + if cp.Properties.MaximumPacketSize != nil { + c.clientProps.MaximumPacketSize = *cp.Properties.MaximumPacketSize + } + if cp.Properties.MaximumQOS != nil { + c.clientProps.MaximumQoS = *cp.Properties.MaximumQOS + } + if cp.Properties.ReceiveMaximum != nil { + c.clientProps.ReceiveMaximum = *cp.Properties.ReceiveMaximum + } + if cp.Properties.TopicAliasMaximum != nil { + c.clientProps.TopicAliasMaximum = *cp.Properties.TopicAliasMaximum + } + } + + c.debug.Println("connecting") + connCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) + defer cf() + + ccp := cp.Packet() + ccp.ProtocolName = "MQTT" + ccp.ProtocolVersion = 5 + + c.debug.Println("sending CONNECT") + if _, err := ccp.WriteTo(c.Conn); err != nil { + cleanup() + return nil, err + } + + c.debug.Println("waiting for CONNACK/AUTH") + var ( + caPacket *packets.Connack + // We use buffered channels to prevent goroutine leak. The Details are below. + // - c.expectConnack waits to send data to caPacketCh or caPacketErr. + // - If connCtx is cancelled (done) before c.expectConnack finishes to send data to either "unbuffered" channel, + // c.expectConnack cannot exit (goroutine leak). + caPacketCh = make(chan *packets.Connack, 1) + caPacketErr = make(chan error, 1) + ) + go c.expectConnack(caPacketCh, caPacketErr) + select { + case <-connCtx.Done(): + if ctxErr := connCtx.Err(); ctxErr != nil { + c.debug.Println(fmt.Sprintf("terminated due to context: %v", ctxErr)) + } + cleanup() + return nil, connCtx.Err() + case err := <-caPacketErr: + c.debug.Println(err) + cleanup() + return nil, err + case caPacket = <-caPacketCh: + } + + ca := ConnackFromPacketConnack(caPacket) + + if ca.ReasonCode >= 0x80 { + var reason string + c.debug.Println("received an error code in Connack:", ca.ReasonCode) + if ca.Properties != nil { + reason = ca.Properties.ReasonString + } + cleanup() + return ca, fmt.Errorf("failed to connect to server: %s", reason) + } + + // no more possible calls to cleanup(), defer an unlock + defer c.mu.Unlock() + + if ca.Properties != nil { + if ca.Properties.ServerKeepAlive != nil { + keepalive = *ca.Properties.ServerKeepAlive + } + if ca.Properties.AssignedClientID != "" { + c.ClientID = ca.Properties.AssignedClientID + } + if ca.Properties.ReceiveMaximum != nil { + c.serverProps.ReceiveMaximum = *ca.Properties.ReceiveMaximum + } + if ca.Properties.MaximumQoS != nil { + c.serverProps.MaximumQoS = *ca.Properties.MaximumQoS + } + if ca.Properties.MaximumPacketSize != nil { + c.serverProps.MaximumPacketSize = *ca.Properties.MaximumPacketSize + } + if ca.Properties.TopicAliasMaximum != nil { + c.serverProps.TopicAliasMaximum = *ca.Properties.TopicAliasMaximum + } + c.serverProps.RetainAvailable = ca.Properties.RetainAvailable + c.serverProps.WildcardSubAvailable = ca.Properties.WildcardSubAvailable + c.serverProps.SubIDAvailable = ca.Properties.SubIDAvailable + c.serverProps.SharedSubAvailable = ca.Properties.SharedSubAvailable + } + + c.serverInflight = semaphore.NewWeighted(int64(c.serverProps.ReceiveMaximum)) + c.clientInflight = semaphore.NewWeighted(int64(c.clientProps.ReceiveMaximum)) + + c.debug.Println("received CONNACK, starting PingHandler") + c.workers.Add(1) + go func() { + defer c.workers.Done() + defer c.debug.Println("returning from ping handler worker") + c.PingHandler.Start(c.Conn, time.Duration(keepalive)*time.Second) + }() + + c.debug.Println("starting publish packets loop") + c.workers.Add(1) + go func() { + defer c.workers.Done() + defer c.debug.Println("returning from publish packets loop worker") + c.routePublishPackets() + }() + + c.debug.Println("starting incoming") + c.workers.Add(1) + go func() { + defer c.workers.Done() + defer c.debug.Println("returning from incoming worker") + c.incoming() + }() + + if c.EnableManualAcknowledgment { + c.debug.Println("starting acking routine") + + c.acksTracker.reset() + sendAcksInterval := defaultSendAckInterval + if c.SendAcksInterval > 0 { + sendAcksInterval = c.SendAcksInterval + } + + c.workers.Add(1) + go func() { + defer c.workers.Done() + defer c.debug.Println("returning from ack tracker routine") + t := time.NewTicker(sendAcksInterval) + for { + select { + case <-c.stop: + return + case <-t.C: + c.acksTracker.flush(func(pbs []*packets.Publish) { + for _, pb := range pbs { + c.ack(pb) + } + }) + } + } + }() + } + + return ca, nil +} + +func (c *Client) Ack(pb *Publish) error { + if !c.EnableManualAcknowledgment { + return ErrManualAcknowledgmentDisabled + } + if pb.QoS == 0 { + return nil + } + return c.acksTracker.markAsAcked(pb.Packet()) +} + +func (c *Client) ack(pb *packets.Publish) { + switch pb.QoS { + case 1: + pa := packets.Puback{ + Properties: &packets.Properties{}, + PacketID: pb.PacketID, + } + c.debug.Println("sending PUBACK") + _, err := pa.WriteTo(c.Conn) + if err != nil { + c.errors.Printf("failed to send PUBACK for %d: %s", pb.PacketID, err) + } + case 2: + pr := packets.Pubrec{ + Properties: &packets.Properties{}, + PacketID: pb.PacketID, + } + c.debug.Printf("sending PUBREC") + _, err := pr.WriteTo(c.Conn) + if err != nil { + c.errors.Printf("failed to send PUBREC for %d: %s", pb.PacketID, err) + } + } +} + +func (c *Client) routePublishPackets() { + for { + select { + case <-c.stop: + return + case pb, open := <-c.publishPackets: + if !open { + return + } + + if !c.ClientConfig.EnableManualAcknowledgment { + c.Router.Route(pb) + c.ack(pb) + continue + } + + if pb.QoS != 0 { + c.acksTracker.add(pb) + } + + c.Router.Route(pb) + } + } +} + +// incoming is the Client function that reads and handles incoming +// packets from the server. The function is started as a goroutine +// from Connect(), it exits when it receives a server initiated +// Disconnect, the Stop channel is closed or there is an error reading +// a packet from the network connection +func (c *Client) incoming() { + defer c.debug.Println("client stopping, incoming stopping") + for { + select { + case <-c.stop: + return + default: + recv, err := packets.ReadPacket(c.Conn) + if err != nil { + go c.error(err) + return + } + switch recv.Type { + case packets.CONNACK: + c.debug.Println("received CONNACK") + go c.error(fmt.Errorf("received unexpected CONNACK")) + return + case packets.AUTH: + c.debug.Println("received AUTH") + ap := recv.Content.(*packets.Auth) + switch ap.ReasonCode { + case packets.AuthSuccess: + if c.AuthHandler != nil { + go c.AuthHandler.Authenticated() + } + if c.raCtx != nil { + c.raCtx.Return <- *recv + } + case packets.AuthContinueAuthentication: + if c.AuthHandler != nil { + if _, err := c.AuthHandler.Authenticate(AuthFromPacketAuth(ap)).Packet().WriteTo(c.Conn); err != nil { + go c.error(err) + return + } + } + } + case packets.PUBLISH: + pb := recv.Content.(*packets.Publish) + c.debug.Printf("received QoS%d PUBLISH", pb.QoS) + c.mu.Lock() + select { + case <-c.stop: + c.mu.Unlock() + return + default: + c.publishPackets <- pb + c.mu.Unlock() + } + case packets.PUBACK, packets.PUBCOMP, packets.SUBACK, packets.UNSUBACK: + c.debug.Printf("received %s packet with id %d", recv.PacketType(), recv.PacketID()) + if cpCtx := c.MIDs.Get(recv.PacketID()); cpCtx != nil { + cpCtx.Return <- *recv + } else { + c.debug.Println("received a response for a message ID we don't know:", recv.PacketID()) + } + case packets.PUBREC: + c.debug.Println("received PUBREC for", recv.PacketID()) + if cpCtx := c.MIDs.Get(recv.PacketID()); cpCtx == nil { + c.debug.Println("received a PUBREC for a message ID we don't know:", recv.PacketID()) + pl := packets.Pubrel{ + PacketID: recv.Content.(*packets.Pubrec).PacketID, + ReasonCode: 0x92, + } + c.debug.Println("sending PUBREL for", pl.PacketID) + _, err := pl.WriteTo(c.Conn) + if err != nil { + c.errors.Printf("failed to send PUBREL for %d: %s", pl.PacketID, err) + } + } else { + pr := recv.Content.(*packets.Pubrec) + if pr.ReasonCode >= 0x80 { + //Received a failure code, shortcut and return + cpCtx.Return <- *recv + } else { + pl := packets.Pubrel{ + PacketID: pr.PacketID, + } + c.debug.Println("sending PUBREL for", pl.PacketID) + _, err := pl.WriteTo(c.Conn) + if err != nil { + c.errors.Printf("failed to send PUBREL for %d: %s", pl.PacketID, err) + } + } + } + case packets.PUBREL: + c.debug.Println("received PUBREL for", recv.PacketID()) + //Auto respond to pubrels unless failure code + pr := recv.Content.(*packets.Pubrel) + if pr.ReasonCode >= 0x80 { + //Received a failure code, continue + continue + } else { + pc := packets.Pubcomp{ + PacketID: pr.PacketID, + } + c.debug.Println("sending PUBCOMP for", pr.PacketID) + _, err := pc.WriteTo(c.Conn) + if err != nil { + c.errors.Printf("failed to send PUBCOMP for %d: %s", pc.PacketID, err) + } + } + case packets.DISCONNECT: + c.debug.Println("received DISCONNECT") + if c.raCtx != nil { + c.raCtx.Return <- *recv + } + go func() { + if c.OnServerDisconnect != nil { + go c.serverDisconnect(DisconnectFromPacketDisconnect(recv.Content.(*packets.Disconnect))) + } else { + go c.error(fmt.Errorf("server initiated disconnect")) + } + }() + return + case packets.PINGRESP: + c.debug.Println("received PINGRESP") + c.PingHandler.PingResp() + } + } + } +} + +func (c *Client) close() { + c.mu.Lock() + defer c.mu.Unlock() + + select { + case <-c.stop: + //already shutting down, do nothing + return + default: + } + + close(c.stop) + close(c.publishPackets) + + c.debug.Println("client stopped") + c.PingHandler.Stop() + c.debug.Println("ping stopped") + _ = c.Conn.Close() + c.debug.Println("conn closed") + c.acksTracker.reset() + c.debug.Println("acks tracker reset") +} + +// error is called to signify that an error situation has occurred, this +// causes the client's Stop channel to be closed (if it hasn't already been) +// which results in the other client goroutines terminating. +// It also closes the client network connection. +func (c *Client) error(e error) { + c.debug.Println("error called:", e) + c.close() + c.workers.Wait() + go c.OnClientError(e) +} + +func (c *Client) serverDisconnect(d *Disconnect) { + c.close() + c.workers.Wait() + c.debug.Println("calling OnServerDisconnect") + go c.OnServerDisconnect(d) +} + +// Authenticate is used to initiate a reauthentication of credentials with the +// server. This function sends the initial Auth packet to start the reauthentication +// then relies on the client AuthHandler managing any further requests from the +// server until either a successful Auth packet is passed back, or a Disconnect +// is received. +func (c *Client) Authenticate(ctx context.Context, a *Auth) (*AuthResponse, error) { + c.debug.Println("client initiated reauthentication") + + c.mu.Lock() + if c.raCtx != nil { + c.mu.Unlock() + return nil, fmt.Errorf("previous authentication is still in progress") + } + c.raCtx = &CPContext{ctx, make(chan packets.ControlPacket, 1)} + c.mu.Unlock() + defer func() { + c.mu.Lock() + c.raCtx = nil + c.mu.Unlock() + }() + + c.debug.Println("sending AUTH") + if _, err := a.Packet().WriteTo(c.Conn); err != nil { + return nil, err + } + + var rp packets.ControlPacket + select { + case <-ctx.Done(): + if ctxErr := ctx.Err(); ctxErr != nil { + c.debug.Println(fmt.Sprintf("terminated due to context: %v", ctxErr)) + return nil, ctxErr + } + case rp = <-c.raCtx.Return: + } + + switch rp.Type { + case packets.AUTH: + //If we've received one here it must be successful, the only way + //to abort a reauth is a server initiated disconnect + return AuthResponseFromPacketAuth(rp.Content.(*packets.Auth)), nil + case packets.DISCONNECT: + return AuthResponseFromPacketDisconnect(rp.Content.(*packets.Disconnect)), nil + } + + return nil, fmt.Errorf("error with Auth, didn't receive Auth or Disconnect") +} + +// Subscribe is used to send a Subscription request to the MQTT server. +// It is passed a pre-prepared Subscribe packet and blocks waiting for +// a response Suback, or for the timeout to fire. Any response Suback +// is returned from the function, along with any errors. +func (c *Client) Subscribe(ctx context.Context, s *Subscribe) (*Suback, error) { + if !c.serverProps.WildcardSubAvailable { + for _, sub := range s.Subscriptions { + if strings.ContainsAny(sub.Topic, "#+") { + // Using a wildcard in a subscription when not supported + return nil, fmt.Errorf("cannot subscribe to %s, server does not support wildcards", sub.Topic) + } + } + } + if !c.serverProps.SubIDAvailable && s.Properties != nil && s.Properties.SubscriptionIdentifier != nil { + return nil, fmt.Errorf("cannot send subscribe with subID set, server does not support subID") + } + if !c.serverProps.SharedSubAvailable { + for _, sub := range s.Subscriptions { + if strings.HasPrefix(sub.Topic, "$share") { + return nil, fmt.Errorf("cannont subscribe to %s, server does not support shared subscriptions", sub.Topic) + } + } + } + + c.debug.Printf("subscribing to %+v", s.Subscriptions) + + subCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) + defer cf() + cpCtx := &CPContext{subCtx, make(chan packets.ControlPacket, 1)} + + sp := s.Packet() + + mid, err := c.MIDs.Request(cpCtx) + if err != nil { + return nil, err + } + defer c.MIDs.Free(mid) + sp.PacketID = mid + + c.debug.Println("sending SUBSCRIBE") + if _, err := sp.WriteTo(c.Conn); err != nil { + return nil, err + } + c.debug.Println("waiting for SUBACK") + var sap packets.ControlPacket + + select { + case <-subCtx.Done(): + if ctxErr := subCtx.Err(); ctxErr != nil { + c.debug.Println(fmt.Sprintf("terminated due to context: %v", ctxErr)) + return nil, ctxErr + } + case sap = <-cpCtx.Return: + } + + if sap.Type != packets.SUBACK { + return nil, fmt.Errorf("received %d instead of Suback", sap.Type) + } + c.debug.Println("received SUBACK") + + sa := SubackFromPacketSuback(sap.Content.(*packets.Suback)) + switch { + case len(sa.Reasons) == 1: + if sa.Reasons[0] >= 0x80 { + var reason string + c.debug.Println("received an error code in Suback:", sa.Reasons[0]) + if sa.Properties != nil { + reason = sa.Properties.ReasonString + } + return sa, fmt.Errorf("failed to subscribe to topic: %s", reason) + } + default: + for _, code := range sa.Reasons { + if code >= 0x80 { + c.debug.Println("received an error code in Suback:", code) + return sa, fmt.Errorf("at least one requested subscription failed") + } + } + } + + return sa, nil +} + +// Unsubscribe is used to send an Unsubscribe request to the MQTT server. +// It is passed a pre-prepared Unsubscribe packet and blocks waiting for +// a response Unsuback, or for the timeout to fire. Any response Unsuback +// is returned from the function, along with any errors. +func (c *Client) Unsubscribe(ctx context.Context, u *Unsubscribe) (*Unsuback, error) { + c.debug.Printf("unsubscribing from %+v", u.Topics) + unsubCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) + defer cf() + cpCtx := &CPContext{unsubCtx, make(chan packets.ControlPacket, 1)} + + up := u.Packet() + + mid, err := c.MIDs.Request(cpCtx) + if err != nil { + return nil, err + } + defer c.MIDs.Free(mid) + up.PacketID = mid + + c.debug.Println("sending UNSUBSCRIBE") + if _, err := up.WriteTo(c.Conn); err != nil { + return nil, err + } + c.debug.Println("waiting for UNSUBACK") + var uap packets.ControlPacket + + select { + case <-unsubCtx.Done(): + if ctxErr := unsubCtx.Err(); ctxErr != nil { + c.debug.Println(fmt.Sprintf("terminated due to context: %v", ctxErr)) + return nil, ctxErr + } + case uap = <-cpCtx.Return: + } + + if uap.Type != packets.UNSUBACK { + return nil, fmt.Errorf("received %d instead of Unsuback", uap.Type) + } + c.debug.Println("received SUBACK") + + ua := UnsubackFromPacketUnsuback(uap.Content.(*packets.Unsuback)) + switch { + case len(ua.Reasons) == 1: + if ua.Reasons[0] >= 0x80 { + var reason string + c.debug.Println("received an error code in Unsuback:", ua.Reasons[0]) + if ua.Properties != nil { + reason = ua.Properties.ReasonString + } + return ua, fmt.Errorf("failed to unsubscribe from topic: %s", reason) + } + default: + for _, code := range ua.Reasons { + if code >= 0x80 { + c.debug.Println("received an error code in Suback:", code) + return ua, fmt.Errorf("at least one requested unsubscribe failed") + } + } + } + + return ua, nil +} + +// Publish is used to send a publication to the MQTT server. +// It is passed a pre-prepared Publish packet and blocks waiting for +// the appropriate response, or for the timeout to fire. +// Any response message is returned from the function, along with any errors. +func (c *Client) Publish(ctx context.Context, p *Publish) (*PublishResponse, error) { + if p.QoS > c.serverProps.MaximumQoS { + return nil, fmt.Errorf("cannot send Publish with QoS %d, server maximum QoS is %d", p.QoS, c.serverProps.MaximumQoS) + } + if p.Properties != nil && p.Properties.TopicAlias != nil { + if c.serverProps.TopicAliasMaximum > 0 && *p.Properties.TopicAlias > c.serverProps.TopicAliasMaximum { + return nil, fmt.Errorf("cannot send publish with TopicAlias %d, server topic alias maximum is %d", *p.Properties.TopicAlias, c.serverProps.TopicAliasMaximum) + } + } + if !c.serverProps.RetainAvailable && p.Retain { + return nil, fmt.Errorf("cannot send Publish with retain flag set, server does not support retained messages") + } + if (p.Properties == nil || p.Properties.TopicAlias == nil) && p.Topic == "" { + return nil, fmt.Errorf("cannot send a publish with no TopicAlias and no Topic set") + } + + if c.ClientConfig.PublishHook != nil { + c.ClientConfig.PublishHook(p) + } + + c.debug.Printf("sending message to %s", p.Topic) + + pb := p.Packet() + + switch p.QoS { + case 0: + c.debug.Println("sending QoS0 message") + if _, err := pb.WriteTo(c.Conn); err != nil { + return nil, err + } + return nil, nil + case 1, 2: + return c.publishQoS12(ctx, pb) + } + + return nil, fmt.Errorf("QoS isn't 0, 1 or 2") +} + +func (c *Client) publishQoS12(ctx context.Context, pb *packets.Publish) (*PublishResponse, error) { + c.debug.Println("sending QoS12 message") + pubCtx, cf := context.WithTimeout(ctx, c.PacketTimeout) + defer cf() + if err := c.serverInflight.Acquire(pubCtx, 1); err != nil { + return nil, err + } + defer c.serverInflight.Release(1) + cpCtx := &CPContext{pubCtx, make(chan packets.ControlPacket, 1)} + + mid, err := c.MIDs.Request(cpCtx) + if err != nil { + return nil, err + } + defer c.MIDs.Free(mid) + pb.PacketID = mid + + if _, err := pb.WriteTo(c.Conn); err != nil { + return nil, err + } + var resp packets.ControlPacket + + select { + case <-pubCtx.Done(): + if ctxErr := pubCtx.Err(); ctxErr != nil { + c.debug.Println(fmt.Sprintf("terminated due to context: %v", ctxErr)) + return nil, ctxErr + } + case resp = <-cpCtx.Return: + } + + switch pb.QoS { + case 1: + if resp.Type != packets.PUBACK { + return nil, fmt.Errorf("received %d instead of PUBACK", resp.Type) + } + + pr := PublishResponseFromPuback(resp.Content.(*packets.Puback)) + if pr.ReasonCode >= 0x80 { + c.debug.Println("received an error code in Puback:", pr.ReasonCode) + return pr, fmt.Errorf("error publishing: %s", resp.Content.(*packets.Puback).Reason()) + } + return pr, nil + case 2: + switch resp.Type { + case packets.PUBCOMP: + pr := PublishResponseFromPubcomp(resp.Content.(*packets.Pubcomp)) + return pr, nil + case packets.PUBREC: + c.debug.Printf("received PUBREC for %s (must have errored)", pb.PacketID) + pr := PublishResponseFromPubrec(resp.Content.(*packets.Pubrec)) + return pr, nil + default: + return nil, fmt.Errorf("received %d instead of PUBCOMP", resp.Type) + } + } + + c.debug.Println("ended up with a non QoS1/2 message:", pb.QoS) + return nil, fmt.Errorf("ended up with a non QoS1/2 message: %d", pb.QoS) +} + +func (c *Client) expectConnack(packet chan<- *packets.Connack, errs chan<- error) { + recv, err := packets.ReadPacket(c.Conn) + if err != nil { + errs <- err + return + } + switch r := recv.Content.(type) { + case *packets.Connack: + c.debug.Println("received CONNACK") + if r.ReasonCode == packets.ConnackSuccess && r.Properties != nil && r.Properties.AuthMethod != "" { + // Successful connack and AuthMethod is defined, must have successfully authed during connect + go c.AuthHandler.Authenticated() + } + packet <- r + case *packets.Auth: + c.debug.Println("received AUTH") + if c.AuthHandler == nil { + errs <- fmt.Errorf("enhanced authentication flow started but no AuthHandler configured") + return + } + c.debug.Println("sending AUTH") + _, err := c.AuthHandler.Authenticate(AuthFromPacketAuth(r)).Packet().WriteTo(c.Conn) + if err != nil { + errs <- fmt.Errorf("error sending authentication packet: %w", err) + return + } + // go round again, either another AUTH or CONNACK + go c.expectConnack(packet, errs) + default: + errs <- fmt.Errorf("received unexpected packet %v", recv.Type) + } + +} + +// Disconnect is used to send a Disconnect packet to the MQTT server +// Whether or not the attempt to send the Disconnect packet fails +// (and if it does this function returns any error) the network connection +// is closed. +func (c *Client) Disconnect(d *Disconnect) error { + c.debug.Println("disconnecting") + _, err := d.Packet().WriteTo(c.Conn) + + c.close() + c.workers.Wait() + + return err +} + +// SetDebugLogger takes an instance of the paho Logger interface +// and sets it to be used by the debug log endpoint +func (c *Client) SetDebugLogger(l Logger) { + c.debug = l +} + +// SetErrorLogger takes an instance of the paho Logger interface +// and sets it to be used by the error log endpoint +func (c *Client) SetErrorLogger(l Logger) { + c.errors = l +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_auth.go b/vendor/github.com/eclipse/paho.golang/paho/cp_auth.go new file mode 100644 index 00000000000..6ccef9b47de --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_auth.go @@ -0,0 +1,92 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // Auth is a representation of the MQTT Auth packet + Auth struct { + Properties *AuthProperties + ReasonCode byte + } + + // AuthProperties is a struct of the properties that can be set + // for a Auth packet + AuthProperties struct { + AuthData []byte + AuthMethod string + ReasonString string + User UserProperties + } +) + +// InitProperties is a function that takes a lower level +// Properties struct and completes the properties of the Auth on +// which it is called +func (a *Auth) InitProperties(p *packets.Properties) { + a.Properties = &AuthProperties{ + AuthMethod: p.AuthMethod, + AuthData: p.AuthData, + ReasonString: p.ReasonString, + User: UserPropertiesFromPacketUser(p.User), + } +} + +// AuthFromPacketAuth takes a packets library Auth and +// returns a paho library Auth +func AuthFromPacketAuth(a *packets.Auth) *Auth { + v := &Auth{ReasonCode: a.ReasonCode} + v.InitProperties(a.Properties) + + return v +} + +// Packet returns a packets library Auth from the paho Auth +// on which it is called +func (a *Auth) Packet() *packets.Auth { + v := &packets.Auth{ReasonCode: a.ReasonCode} + + if a.Properties != nil { + v.Properties = &packets.Properties{ + AuthMethod: a.Properties.AuthMethod, + AuthData: a.Properties.AuthData, + ReasonString: a.Properties.ReasonString, + User: a.Properties.User.ToPacketProperties(), + } + } + + return v +} + +// AuthResponse is a represenation of the response to an Auth +// packet +type AuthResponse struct { + Properties *AuthProperties + ReasonCode byte + Success bool +} + +// AuthResponseFromPacketAuth takes a packets library Auth and +// returns a paho library AuthResponse +func AuthResponseFromPacketAuth(a *packets.Auth) *AuthResponse { + return &AuthResponse{ + Success: true, + ReasonCode: a.ReasonCode, + Properties: &AuthProperties{ + ReasonString: a.Properties.ReasonString, + User: UserPropertiesFromPacketUser(a.Properties.User), + }, + } +} + +// AuthResponseFromPacketDisconnect takes a packets library Disconnect and +// returns a paho library AuthResponse +func AuthResponseFromPacketDisconnect(d *packets.Disconnect) *AuthResponse { + return &AuthResponse{ + Success: true, + ReasonCode: d.ReasonCode, + Properties: &AuthProperties{ + ReasonString: d.Properties.ReasonString, + User: UserPropertiesFromPacketUser(d.Properties.User), + }, + } +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_connack.go b/vendor/github.com/eclipse/paho.golang/paho/cp_connack.go new file mode 100644 index 00000000000..2a525eb48b0 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_connack.go @@ -0,0 +1,142 @@ +package paho + +import ( + "fmt" + "strings" + + "github.com/eclipse/paho.golang/packets" +) + +type ( + // Connack is a representation of the MQTT Connack packet + Connack struct { + Properties *ConnackProperties + ReasonCode byte + SessionPresent bool + } + + // ConnackProperties is a struct of the properties that can be set + // for a Connack packet + ConnackProperties struct { + SessionExpiryInterval *uint32 + AuthData []byte + AuthMethod string + ResponseInfo string + ServerReference string + ReasonString string + AssignedClientID string + MaximumPacketSize *uint32 + ReceiveMaximum *uint16 + TopicAliasMaximum *uint16 + ServerKeepAlive *uint16 + MaximumQoS *byte + User UserProperties + WildcardSubAvailable bool + SubIDAvailable bool + SharedSubAvailable bool + RetainAvailable bool + } +) + +// InitProperties is a function that takes a lower level +// Properties struct and completes the properties of the Connack on +// which it is called +func (c *Connack) InitProperties(p *packets.Properties) { + c.Properties = &ConnackProperties{ + AssignedClientID: p.AssignedClientID, + ServerKeepAlive: p.ServerKeepAlive, + WildcardSubAvailable: true, + SubIDAvailable: true, + SharedSubAvailable: true, + RetainAvailable: true, + ResponseInfo: p.ResponseInfo, + SessionExpiryInterval: p.SessionExpiryInterval, + AuthMethod: p.AuthMethod, + AuthData: p.AuthData, + ServerReference: p.ServerReference, + ReasonString: p.ReasonString, + ReceiveMaximum: p.ReceiveMaximum, + TopicAliasMaximum: p.TopicAliasMaximum, + MaximumQoS: p.MaximumQOS, + MaximumPacketSize: p.MaximumPacketSize, + User: UserPropertiesFromPacketUser(p.User), + } + + if p.WildcardSubAvailable != nil { + c.Properties.WildcardSubAvailable = *p.WildcardSubAvailable == 1 + } + if p.SubIDAvailable != nil { + c.Properties.SubIDAvailable = *p.SubIDAvailable == 1 + } + if p.SharedSubAvailable != nil { + c.Properties.SharedSubAvailable = *p.SharedSubAvailable == 1 + } + if p.RetainAvailable != nil { + c.Properties.RetainAvailable = *p.RetainAvailable == 1 + } +} + +// ConnackFromPacketConnack takes a packets library Connack and +// returns a paho library Connack +func ConnackFromPacketConnack(c *packets.Connack) *Connack { + v := &Connack{ + SessionPresent: c.SessionPresent, + ReasonCode: c.ReasonCode, + } + v.InitProperties(c.Properties) + + return v +} + +// String implement fmt.Stringer (mainly to simplify debugging) +func (c *Connack) String() string { + return fmt.Sprintf("CONNACK: ReasonCode:%d SessionPresent:%t\nProperties:\n%s", c.ReasonCode, c.SessionPresent, c.Properties) +} + +// String implement fmt.Stringer (mainly to simplify debugging) +func (p *ConnackProperties) String() string { + var b strings.Builder + if p.SessionExpiryInterval != nil { + fmt.Fprintf(&b, "\tSessionExpiryInterval:%d\n", *p.SessionExpiryInterval) + } + if p.AssignedClientID != "" { + fmt.Fprintf(&b, "\tAssignedClientID:%s\n", p.AssignedClientID) + } + if p.ServerKeepAlive != nil { + fmt.Fprintf(&b, "\tServerKeepAlive:%d\n", *p.ServerKeepAlive) + } + if p.AuthMethod != "" { + fmt.Fprintf(&b, "\tAuthMethod:%s\n", p.AuthMethod) + } + if len(p.AuthData) > 0 { + fmt.Fprintf(&b, "\tAuthData:%X\n", p.AuthData) + } + if p.ServerReference != "" { + fmt.Fprintf(&b, "\tServerReference:%s\n", p.ServerReference) + } + if p.ReasonString != "" { + fmt.Fprintf(&b, "\tReasonString:%s\n", p.ReasonString) + } + if p.ReceiveMaximum != nil { + fmt.Fprintf(&b, "\tReceiveMaximum:%d\n", *p.ReceiveMaximum) + } + if p.TopicAliasMaximum != nil { + fmt.Fprintf(&b, "\tTopicAliasMaximum:%d\n", *p.TopicAliasMaximum) + } + fmt.Fprintf(&b, "\tRetainAvailable:%t\n", p.RetainAvailable) + if p.MaximumPacketSize != nil { + fmt.Fprintf(&b, "\tMaximumPacketSize:%d\n", *p.MaximumPacketSize) + } + fmt.Fprintf(&b, "\tWildcardSubAvailable:%t\n", p.WildcardSubAvailable) + fmt.Fprintf(&b, "\tSubIDAvailable:%t\n", p.SubIDAvailable) + fmt.Fprintf(&b, "\tSharedSubAvailable:%t\n", p.SharedSubAvailable) + + if len(p.User) > 0 { + fmt.Fprint(&b, "\tUser Properties:\n") + for _, v := range p.User { + fmt.Fprintf(&b, "\t\t%s:%s\n", v.Key, v.Value) + } + } + + return b.String() +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_connect.go b/vendor/github.com/eclipse/paho.golang/paho/cp_connect.go new file mode 100644 index 00000000000..8d731764d14 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_connect.go @@ -0,0 +1,180 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // Connect is a representation of the MQTT Connect packet + Connect struct { + Password []byte + Username string + ClientID string + Properties *ConnectProperties + WillMessage *WillMessage + WillProperties *WillProperties + KeepAlive uint16 + CleanStart bool + UsernameFlag bool + PasswordFlag bool + } + + // ConnectProperties is a struct of the properties that can be set + // for a Connect packet + ConnectProperties struct { + AuthData []byte + AuthMethod string + SessionExpiryInterval *uint32 + WillDelayInterval *uint32 + ReceiveMaximum *uint16 + TopicAliasMaximum *uint16 + MaximumQOS *byte + MaximumPacketSize *uint32 + User UserProperties + RequestProblemInfo bool + RequestResponseInfo bool + } +) + +// InitProperties is a function that takes a lower level +// Properties struct and completes the properties of the Connect on +// which it is called +func (c *Connect) InitProperties(p *packets.Properties) { + c.Properties = &ConnectProperties{ + SessionExpiryInterval: p.SessionExpiryInterval, + AuthMethod: p.AuthMethod, + AuthData: p.AuthData, + WillDelayInterval: p.WillDelayInterval, + RequestResponseInfo: false, + RequestProblemInfo: true, + ReceiveMaximum: p.ReceiveMaximum, + TopicAliasMaximum: p.TopicAliasMaximum, + MaximumQOS: p.MaximumQOS, + MaximumPacketSize: p.MaximumPacketSize, + User: UserPropertiesFromPacketUser(p.User), + } + + if p.RequestResponseInfo != nil { + c.Properties.RequestResponseInfo = *p.RequestProblemInfo == 1 + } + if p.RequestProblemInfo != nil { + c.Properties.RequestProblemInfo = *p.RequestProblemInfo == 1 + } +} + +// InitWillProperties is a function that takes a lower level +// Properties struct and completes the properties of the Will in the Connect on +// which it is called +func (c *Connect) InitWillProperties(p *packets.Properties) { + c.WillProperties = &WillProperties{ + WillDelayInterval: p.WillDelayInterval, + PayloadFormat: p.PayloadFormat, + MessageExpiry: p.MessageExpiry, + ContentType: p.ContentType, + ResponseTopic: p.ResponseTopic, + CorrelationData: p.CorrelationData, + User: UserPropertiesFromPacketUser(p.User), + } +} + +// ConnectFromPacketConnect takes a packets library Connect and +// returns a paho library Connect +func ConnectFromPacketConnect(p *packets.Connect) *Connect { + v := &Connect{ + UsernameFlag: p.UsernameFlag, + Username: p.Username, + PasswordFlag: p.PasswordFlag, + Password: p.Password, + ClientID: p.ClientID, + CleanStart: p.CleanStart, + KeepAlive: p.KeepAlive, + } + v.InitProperties(p.Properties) + if p.WillFlag { + v.WillMessage = &WillMessage{ + Retain: p.WillRetain, + QoS: p.WillQOS, + Topic: p.WillTopic, + Payload: p.WillMessage, + } + v.InitWillProperties(p.WillProperties) + } + + return v +} + +// Packet returns a packets library Connect from the paho Connect +// on which it is called +func (c *Connect) Packet() *packets.Connect { + v := &packets.Connect{ + UsernameFlag: c.UsernameFlag, + Username: c.Username, + PasswordFlag: c.PasswordFlag, + Password: c.Password, + ClientID: c.ClientID, + CleanStart: c.CleanStart, + KeepAlive: c.KeepAlive, + } + + if c.Properties != nil { + v.Properties = &packets.Properties{ + SessionExpiryInterval: c.Properties.SessionExpiryInterval, + AuthMethod: c.Properties.AuthMethod, + AuthData: c.Properties.AuthData, + WillDelayInterval: c.Properties.WillDelayInterval, + ReceiveMaximum: c.Properties.ReceiveMaximum, + TopicAliasMaximum: c.Properties.TopicAliasMaximum, + MaximumQOS: c.Properties.MaximumQOS, + MaximumPacketSize: c.Properties.MaximumPacketSize, + User: c.Properties.User.ToPacketProperties(), + } + if c.Properties.RequestResponseInfo { + v.Properties.RequestResponseInfo = Byte(1) + } + if !c.Properties.RequestProblemInfo { + v.Properties.RequestProblemInfo = Byte(0) + } + } + + if c.WillMessage != nil { + v.WillFlag = true + v.WillQOS = c.WillMessage.QoS + v.WillTopic = c.WillMessage.Topic + v.WillRetain = c.WillMessage.Retain + v.WillMessage = c.WillMessage.Payload + if c.WillProperties != nil { + v.WillProperties = &packets.Properties{ + WillDelayInterval: c.WillProperties.WillDelayInterval, + PayloadFormat: c.WillProperties.PayloadFormat, + MessageExpiry: c.WillProperties.MessageExpiry, + ContentType: c.WillProperties.ContentType, + ResponseTopic: c.WillProperties.ResponseTopic, + CorrelationData: c.WillProperties.CorrelationData, + User: c.WillProperties.User.ToPacketProperties(), + } + } + } + + return v +} + +type ( + // WillMessage is a representation of the LWT message that can + // be sent with the Connect packet + WillMessage struct { + Retain bool + QoS byte + Topic string + Payload []byte + } + + // WillProperties is a struct of the properties that can be set + // for a Will in a Connect packet + WillProperties struct { + WillDelayInterval *uint32 + PayloadFormat *byte + MessageExpiry *uint32 + ContentType string + ResponseTopic string + CorrelationData []byte + User UserProperties + } +) diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_disconnect.go b/vendor/github.com/eclipse/paho.golang/paho/cp_disconnect.go new file mode 100644 index 00000000000..5caa85b147c --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_disconnect.go @@ -0,0 +1,58 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // Disconnect is a representation of the MQTT Disconnect packet + Disconnect struct { + Properties *DisconnectProperties + ReasonCode byte + } + + // DisconnectProperties is a struct of the properties that can be set + // for a Disconnect packet + DisconnectProperties struct { + ServerReference string + ReasonString string + SessionExpiryInterval *uint32 + User UserProperties + } +) + +// InitProperties is a function that takes a lower level +// Properties struct and completes the properties of the Disconnect on +// which it is called +func (d *Disconnect) InitProperties(p *packets.Properties) { + d.Properties = &DisconnectProperties{ + SessionExpiryInterval: p.SessionExpiryInterval, + ServerReference: p.ServerReference, + ReasonString: p.ReasonString, + User: UserPropertiesFromPacketUser(p.User), + } +} + +// DisconnectFromPacketDisconnect takes a packets library Disconnect and +// returns a paho library Disconnect +func DisconnectFromPacketDisconnect(p *packets.Disconnect) *Disconnect { + v := &Disconnect{ReasonCode: p.ReasonCode} + v.InitProperties(p.Properties) + + return v +} + +// Packet returns a packets library Disconnect from the paho Disconnect +// on which it is called +func (d *Disconnect) Packet() *packets.Disconnect { + v := &packets.Disconnect{ReasonCode: d.ReasonCode} + + if d.Properties != nil { + v.Properties = &packets.Properties{ + SessionExpiryInterval: d.Properties.SessionExpiryInterval, + ServerReference: d.Properties.ServerReference, + ReasonString: d.Properties.ReasonString, + User: d.Properties.User.ToPacketProperties(), + } + } + + return v +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_publish.go b/vendor/github.com/eclipse/paho.golang/paho/cp_publish.go new file mode 100644 index 00000000000..1bb9654b341 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_publish.go @@ -0,0 +1,123 @@ +package paho + +import ( + "bytes" + "fmt" + + "github.com/eclipse/paho.golang/packets" +) + +type ( + // Publish is a representation of the MQTT Publish packet + Publish struct { + PacketID uint16 + QoS byte + Retain bool + Topic string + Properties *PublishProperties + Payload []byte + } + + // PublishProperties is a struct of the properties that can be set + // for a Publish packet + PublishProperties struct { + CorrelationData []byte + ContentType string + ResponseTopic string + PayloadFormat *byte + MessageExpiry *uint32 + SubscriptionIdentifier *int + TopicAlias *uint16 + User UserProperties + } +) + +// InitProperties is a function that takes a lower level +// Properties struct and completes the properties of the Publish on +// which it is called +func (p *Publish) InitProperties(prop *packets.Properties) { + p.Properties = &PublishProperties{ + PayloadFormat: prop.PayloadFormat, + MessageExpiry: prop.MessageExpiry, + ContentType: prop.ContentType, + ResponseTopic: prop.ResponseTopic, + CorrelationData: prop.CorrelationData, + TopicAlias: prop.TopicAlias, + SubscriptionIdentifier: prop.SubscriptionIdentifier, + User: UserPropertiesFromPacketUser(prop.User), + } +} + +// PublishFromPacketPublish takes a packets library Publish and +// returns a paho library Publish +func PublishFromPacketPublish(p *packets.Publish) *Publish { + v := &Publish{ + PacketID: p.PacketID, + QoS: p.QoS, + Retain: p.Retain, + Topic: p.Topic, + Payload: p.Payload, + } + v.InitProperties(p.Properties) + + return v +} + +// Packet returns a packets library Publish from the paho Publish +// on which it is called +func (p *Publish) Packet() *packets.Publish { + v := &packets.Publish{ + PacketID: p.PacketID, + QoS: p.QoS, + Retain: p.Retain, + Topic: p.Topic, + Payload: p.Payload, + } + if p.Properties != nil { + v.Properties = &packets.Properties{ + PayloadFormat: p.Properties.PayloadFormat, + MessageExpiry: p.Properties.MessageExpiry, + ContentType: p.Properties.ContentType, + ResponseTopic: p.Properties.ResponseTopic, + CorrelationData: p.Properties.CorrelationData, + TopicAlias: p.Properties.TopicAlias, + SubscriptionIdentifier: p.Properties.SubscriptionIdentifier, + User: p.Properties.User.ToPacketProperties(), + } + } + + return v +} + +func (p *Publish) String() string { + var b bytes.Buffer + + fmt.Fprintf(&b, "topic: %s qos: %d retain: %t\n", p.Topic, p.QoS, p.Retain) + if p.Properties.PayloadFormat != nil { + fmt.Fprintf(&b, "PayloadFormat: %v\n", p.Properties.PayloadFormat) + } + if p.Properties.MessageExpiry != nil { + fmt.Fprintf(&b, "MessageExpiry: %v\n", p.Properties.MessageExpiry) + } + if p.Properties.ContentType != "" { + fmt.Fprintf(&b, "ContentType: %v\n", p.Properties.ContentType) + } + if p.Properties.ResponseTopic != "" { + fmt.Fprintf(&b, "ResponseTopic: %v\n", p.Properties.ResponseTopic) + } + if p.Properties.CorrelationData != nil { + fmt.Fprintf(&b, "CorrelationData: %v\n", p.Properties.CorrelationData) + } + if p.Properties.TopicAlias != nil { + fmt.Fprintf(&b, "TopicAlias: %d\n", p.Properties.TopicAlias) + } + if p.Properties.SubscriptionIdentifier != nil { + fmt.Fprintf(&b, "SubscriptionIdentifier: %v\n", p.Properties.SubscriptionIdentifier) + } + for _, v := range p.Properties.User { + fmt.Fprintf(&b, "User: %s : %s\n", v.Key, v.Value) + } + b.WriteString(string(p.Payload)) + + return b.String() +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_pubresp.go b/vendor/github.com/eclipse/paho.golang/paho/cp_pubresp.go new file mode 100644 index 00000000000..0c4e174aa00 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_pubresp.go @@ -0,0 +1,55 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // PublishResponse is a generic representation of a response + // to a QoS1 or QoS2 Publish + PublishResponse struct { + Properties *PublishResponseProperties + ReasonCode byte + } + + // PublishResponseProperties is the properties associated with + // a response to a QoS1 or QoS2 Publish + PublishResponseProperties struct { + ReasonString string + User UserProperties + } +) + +// PublishResponseFromPuback takes a packets library Puback and +// returns a paho library PublishResponse +func PublishResponseFromPuback(pa *packets.Puback) *PublishResponse { + return &PublishResponse{ + ReasonCode: pa.ReasonCode, + Properties: &PublishResponseProperties{ + ReasonString: pa.Properties.ReasonString, + User: UserPropertiesFromPacketUser(pa.Properties.User), + }, + } +} + +// PublishResponseFromPubcomp takes a packets library Pubcomp and +// returns a paho library PublishResponse +func PublishResponseFromPubcomp(pc *packets.Pubcomp) *PublishResponse { + return &PublishResponse{ + ReasonCode: pc.ReasonCode, + Properties: &PublishResponseProperties{ + ReasonString: pc.Properties.ReasonString, + User: UserPropertiesFromPacketUser(pc.Properties.User), + }, + } +} + +// PublishResponseFromPubrec takes a packets library Pubrec and +// returns a paho library PublishResponse +func PublishResponseFromPubrec(pr *packets.Pubrec) *PublishResponse { + return &PublishResponse{ + ReasonCode: pr.ReasonCode, + Properties: &PublishResponseProperties{ + ReasonString: pr.Properties.ReasonString, + User: UserPropertiesFromPacketUser(pr.Properties.User), + }, + } +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_suback.go b/vendor/github.com/eclipse/paho.golang/paho/cp_suback.go new file mode 100644 index 00000000000..c1034c26c30 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_suback.go @@ -0,0 +1,41 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // Suback is a representation of an MQTT suback packet + Suback struct { + Properties *SubackProperties + Reasons []byte + } + + // SubackProperties is a struct of the properties that can be set + // for a Suback packet + SubackProperties struct { + ReasonString string + User UserProperties + } +) + +// Packet returns a packets library Suback from the paho Suback +// on which it is called +func (s *Suback) Packet() *packets.Suback { + return &packets.Suback{ + Reasons: s.Reasons, + Properties: &packets.Properties{ + User: s.Properties.User.ToPacketProperties(), + }, + } +} + +// SubackFromPacketSuback takes a packets library Suback and +// returns a paho library Suback +func SubackFromPacketSuback(s *packets.Suback) *Suback { + return &Suback{ + Reasons: s.Reasons, + Properties: &SubackProperties{ + ReasonString: s.Properties.ReasonString, + User: UserPropertiesFromPacketUser(s.Properties.User), + }, + } +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_subscribe.go b/vendor/github.com/eclipse/paho.golang/paho/cp_subscribe.go new file mode 100644 index 00000000000..52dc54017f2 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_subscribe.go @@ -0,0 +1,69 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // Subscribe is a representation of a MQTT subscribe packet + Subscribe struct { + Properties *SubscribeProperties + Subscriptions []SubscribeOptions + } + + // SubscribeOptions is the struct representing the options for a subscription + SubscribeOptions struct { + Topic string + QoS byte + RetainHandling byte + NoLocal bool + RetainAsPublished bool + } +) + +// SubscribeProperties is a struct of the properties that can be set +// for a Subscribe packet +type SubscribeProperties struct { + SubscriptionIdentifier *int + User UserProperties +} + +// InitProperties is a function that takes a packet library +// Properties struct and completes the properties of the Subscribe on +// which it is called +func (s *Subscribe) InitProperties(prop *packets.Properties) { + s.Properties = &SubscribeProperties{ + SubscriptionIdentifier: prop.SubscriptionIdentifier, + User: UserPropertiesFromPacketUser(prop.User), + } +} + +// PacketSubOptionsFromSubscribeOptions returns a slice of packet +// library SubOptions for the paho Subscribe on which it is called +func (s *Subscribe) PacketSubOptionsFromSubscribeOptions() []packets.SubOptions { + r := make([]packets.SubOptions, len(s.Subscriptions)) + for i, sub := range s.Subscriptions { + r[i] = packets.SubOptions{ + Topic: sub.Topic, + QoS: sub.QoS, + NoLocal: sub.NoLocal, + RetainAsPublished: sub.RetainAsPublished, + RetainHandling: sub.RetainHandling, + } + } + + return r +} + +// Packet returns a packets library Subscribe from the paho Subscribe +// on which it is called +func (s *Subscribe) Packet() *packets.Subscribe { + v := &packets.Subscribe{Subscriptions: s.PacketSubOptionsFromSubscribeOptions()} + + if s.Properties != nil { + v.Properties = &packets.Properties{ + SubscriptionIdentifier: s.Properties.SubscriptionIdentifier, + User: s.Properties.User.ToPacketProperties(), + } + } + + return v +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_unsuback.go b/vendor/github.com/eclipse/paho.golang/paho/cp_unsuback.go new file mode 100644 index 00000000000..15ca83885a0 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_unsuback.go @@ -0,0 +1,41 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // Unsuback is a representation of an MQTT Unsuback packet + Unsuback struct { + Reasons []byte + Properties *UnsubackProperties + } + + // UnsubackProperties is a struct of the properties that can be set + // for a Unsuback packet + UnsubackProperties struct { + ReasonString string + User UserProperties + } +) + +// Packet returns a packets library Unsuback from the paho Unsuback +// on which it is called +func (u *Unsuback) Packet() *packets.Unsuback { + return &packets.Unsuback{ + Reasons: u.Reasons, + Properties: &packets.Properties{ + User: u.Properties.User.ToPacketProperties(), + }, + } +} + +// UnsubackFromPacketUnsuback takes a packets library Unsuback and +// returns a paho library Unsuback +func UnsubackFromPacketUnsuback(u *packets.Unsuback) *Unsuback { + return &Unsuback{ + Reasons: u.Reasons, + Properties: &UnsubackProperties{ + ReasonString: u.Properties.ReasonString, + User: UserPropertiesFromPacketUser(u.Properties.User), + }, + } +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_unsubscribe.go b/vendor/github.com/eclipse/paho.golang/paho/cp_unsubscribe.go new file mode 100644 index 00000000000..375b917c8f6 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_unsubscribe.go @@ -0,0 +1,31 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type ( + // Unsubscribe is a representation of an MQTT unsubscribe packet + Unsubscribe struct { + Topics []string + Properties *UnsubscribeProperties + } + + // UnsubscribeProperties is a struct of the properties that can be set + // for a Unsubscribe packet + UnsubscribeProperties struct { + User UserProperties + } +) + +// Packet returns a packets library Unsubscribe from the paho Unsubscribe +// on which it is called +func (u *Unsubscribe) Packet() *packets.Unsubscribe { + v := &packets.Unsubscribe{Topics: u.Topics} + + if u.Properties != nil { + v.Properties = &packets.Properties{ + User: u.Properties.User.ToPacketProperties(), + } + } + + return v +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/cp_utils.go b/vendor/github.com/eclipse/paho.golang/paho/cp_utils.go new file mode 100644 index 00000000000..2d7995f5ca7 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/cp_utils.go @@ -0,0 +1,100 @@ +package paho + +import ( + "github.com/eclipse/paho.golang/packets" +) + +// UserProperty is a struct for the user provided values +// permitted in the properties section +type UserProperty struct { + Key, Value string +} + +// UserProperties is a slice of UserProperty +type UserProperties []UserProperty + +// Add is a helper function for easily adding a new user property +func (u *UserProperties) Add(key, value string) *UserProperties { + *u = append(*u, UserProperty{key, value}) + + return u +} + +// Get returns the first entry in the UserProperties that matches +// key, or an empty string if the key is not found. Note that it is +// permitted to have multiple entries with the same key, use GetAll +// if it is expected to have multiple matches +func (u UserProperties) Get(key string) string { + for _, v := range u { + if v.Key == key { + return v.Value + } + } + + return "" +} + +// GetAll returns a slice of all entries in the UserProperties +// that match key, or a nil slice if none were found. +func (u UserProperties) GetAll(key string) []string { + var ret []string + for _, v := range u { + if v.Key == key { + ret = append(ret, v.Value) + } + } + + return ret +} + +// ToPacketProperties converts a UserProperties to a slice +// of packets.User which is used internally in the packets +// library for user properties +func (u UserProperties) ToPacketProperties() []packets.User { + ret := make([]packets.User, len(u)) + for i, v := range u { + ret[i] = packets.User{Key: v.Key, Value: v.Value} + } + + return ret +} + +// UserPropertiesFromPacketUser converts a slice of packets.User +// to an instance of UserProperties for easier consumption within +// the client library +func UserPropertiesFromPacketUser(up []packets.User) UserProperties { + ret := make(UserProperties, len(up)) + for i, v := range up { + ret[i] = UserProperty{v.Key, v.Value} + } + + return ret +} + +// Byte is a helper function that take a byte and returns +// a pointer to a byte of that value +func Byte(b byte) *byte { + return &b +} + +// Uint32 is a helper function that take a uint32 and returns +// a pointer to a uint32 of that value +func Uint32(u uint32) *uint32 { + return &u +} + +// Uint16 is a helper function that take a uint16 and returns +// a pointer to a uint16 of that value +func Uint16(u uint16) *uint16 { + return &u +} + +// BoolToByte is a helper function that take a bool and returns +// a pointer to a byte of value 1 if true or 0 if false +func BoolToByte(b bool) *byte { + var v byte + if b { + v = 1 + } + return &v +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/message_ids.go b/vendor/github.com/eclipse/paho.golang/paho/message_ids.go new file mode 100644 index 00000000000..ad287b5c755 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/message_ids.go @@ -0,0 +1,110 @@ +package paho + +import ( + "context" + "errors" + "sync" + + "github.com/eclipse/paho.golang/packets" +) + +const ( + midMin uint16 = 1 + midMax uint16 = 65535 +) + +// ErrorMidsExhausted is returned from Request() when there are no +// free message ids to be used. +var ErrorMidsExhausted = errors.New("all message ids in use") + +// MIDService defines the interface for a struct that handles the +// relationship between message ids and CPContexts +// Request() takes a *CPContext and returns a uint16 that is the +// messageid that should be used by the code that called Request() +// Get() takes a uint16 that is a messageid and returns the matching +// *CPContext that the MIDService has associated with that messageid +// Free() takes a uint16 that is a messageid and instructs the MIDService +// to mark that messageid as available for reuse +// Clear() resets the internal state of the MIDService +type MIDService interface { + Request(*CPContext) (uint16, error) + Get(uint16) *CPContext + Free(uint16) + Clear() +} + +// CPContext is the struct that is used to return responses to +// ControlPackets that have them, eg: the suback to a subscribe. +// The response packet is send down the Return channel and the +// Context is used to track timeouts. +type CPContext struct { + Context context.Context + Return chan packets.ControlPacket +} + +// MIDs is the default MIDService provided by this library. +// It uses a slice of *CPContext to track responses +// to messages with a messageid tracking the last used message id +type MIDs struct { + sync.Mutex + lastMid uint16 + index []*CPContext // index of slice is (messageid - 1) +} + +// Request is the library provided MIDService's implementation of +// the required interface function() +func (m *MIDs) Request(c *CPContext) (uint16, error) { + m.Lock() + defer m.Unlock() + + // Scan from lastMid to end of range. + for i := m.lastMid; i < midMax; i++ { + if m.index[i] != nil { + continue + } + m.index[i] = c + m.lastMid = i + 1 + return i + 1, nil + } + // Scan from start of range to lastMid + for i := uint16(0); i < m.lastMid; i++ { + if m.index[i] != nil { + continue + } + m.index[i] = c + m.lastMid = i + 1 + return i + 1, nil + } + + return 0, ErrorMidsExhausted +} + +// Get is the library provided MIDService's implementation of +// the required interface function() +func (m *MIDs) Get(i uint16) *CPContext { + // 0 Packet Identifier is invalid but just in case handled with returning nil to avoid panic. + if i == 0 { + return nil + } + m.Lock() + defer m.Unlock() + return m.index[i-1] +} + +// Free is the library provided MIDService's implementation of +// the required interface function() +func (m *MIDs) Free(i uint16) { + // 0 Packet Identifier is invalid but just in case handled to avoid panic. + if i == 0 { + return + } + m.Lock() + m.index[i-1] = nil + m.Unlock() +} + +// Clear is the library provided MIDService's implementation of +// the required interface function() +func (m *MIDs) Clear() { + m.index = make([]*CPContext, int(midMax)) +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/noop_persistence.go b/vendor/github.com/eclipse/paho.golang/paho/noop_persistence.go new file mode 100644 index 00000000000..d2d15704f1e --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/noop_persistence.go @@ -0,0 +1,23 @@ +package paho + +import "github.com/eclipse/paho.golang/packets" + +type noopPersistence struct{} + +func (n *noopPersistence) Open() {} + +func (n *noopPersistence) Put(id uint16, cp packets.ControlPacket) {} + +func (n *noopPersistence) Get(id uint16) packets.ControlPacket { + return packets.ControlPacket{} +} + +func (n *noopPersistence) All() []packets.ControlPacket { + return nil +} + +func (n *noopPersistence) Delete(id uint16) {} + +func (n *noopPersistence) Close() {} + +func (n *noopPersistence) Reset() {} diff --git a/vendor/github.com/eclipse/paho.golang/paho/persistence.go b/vendor/github.com/eclipse/paho.golang/paho/persistence.go new file mode 100644 index 00000000000..f02b846cc2d --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/persistence.go @@ -0,0 +1,98 @@ +package paho + +import ( + "sync" + + "github.com/eclipse/paho.golang/packets" +) + +// Persistence is an interface of the functions for a struct +// that is used to persist ControlPackets. +// Open() is an initialiser to prepare the Persistence for use +// Put() takes a uint16 which is a messageid and a ControlPacket +// to persist against that messageid +// Get() takes a uint16 which is a messageid and returns the +// persisted ControlPacket from the Persistence for that messageid +// All() returns a slice of all ControlPackets persisted +// Delete() takes a uint16 which is a messageid and deletes the +// associated stored ControlPacket from the Persistence +// Close() closes the Persistence +// Reset() clears the Persistence and prepares it to be reused +type Persistence interface { + Open() + Put(uint16, packets.ControlPacket) + Get(uint16) packets.ControlPacket + All() []packets.ControlPacket + Delete(uint16) + Close() + Reset() +} + +// MemoryPersistence is an implementation of a Persistence +// that stores the ControlPackets in memory using a map +type MemoryPersistence struct { + sync.RWMutex + packets map[uint16]packets.ControlPacket +} + +// Open is the library provided MemoryPersistence's implementation of +// the required interface function() +func (m *MemoryPersistence) Open() { + m.Lock() + m.packets = make(map[uint16]packets.ControlPacket) + m.Unlock() +} + +// Put is the library provided MemoryPersistence's implementation of +// the required interface function() +func (m *MemoryPersistence) Put(id uint16, cp packets.ControlPacket) { + m.Lock() + m.packets[id] = cp + m.Unlock() +} + +// Get is the library provided MemoryPersistence's implementation of +// the required interface function() +func (m *MemoryPersistence) Get(id uint16) packets.ControlPacket { + m.RLock() + defer m.RUnlock() + return m.packets[id] +} + +// All is the library provided MemoryPersistence's implementation of +// the required interface function() +func (m *MemoryPersistence) All() []packets.ControlPacket { + m.Lock() + defer m.RUnlock() + ret := make([]packets.ControlPacket, len(m.packets)) + + for _, cp := range m.packets { + ret = append(ret, cp) + } + + return ret +} + +// Delete is the library provided MemoryPersistence's implementation of +// the required interface function() +func (m *MemoryPersistence) Delete(id uint16) { + m.Lock() + delete(m.packets, id) + m.Unlock() +} + +// Close is the library provided MemoryPersistence's implementation of +// the required interface function() +func (m *MemoryPersistence) Close() { + m.Lock() + m.packets = nil + m.Unlock() +} + +// Reset is the library provided MemoryPersistence's implementation of +// the required interface function() +func (m *MemoryPersistence) Reset() { + m.Lock() + m.packets = make(map[uint16]packets.ControlPacket) + m.Unlock() +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/pinger.go b/vendor/github.com/eclipse/paho.golang/paho/pinger.go new file mode 100644 index 00000000000..e135d25acfe --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/pinger.go @@ -0,0 +1,122 @@ +package paho + +import ( + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/eclipse/paho.golang/packets" +) + +// PingFailHandler is a type for the function that is invoked +// when we have sent a Pingreq to the server and not received +// a Pingresp within 1.5x our pingtimeout +type PingFailHandler func(error) + +// Pinger is an interface of the functions for a struct that is +// used to manage sending PingRequests and responding to +// PingResponses +// Start() takes a net.Conn which is a connection over which an +// MQTT session has already been established, and a time.Duration +// of the keepalive setting passed to the server when the MQTT +// session was established. +// Stop() is used to stop the Pinger +// PingResp() is the function that is called by the Client when +// a PingResponse is received +// SetDebug() is used to pass in a Logger to be used to log debug +// information, for example sharing a logger with the main client +type Pinger interface { + Start(net.Conn, time.Duration) + Stop() + PingResp() + SetDebug(Logger) +} + +// PingHandler is the library provided default Pinger +type PingHandler struct { + mu sync.Mutex + lastPing time.Time + conn net.Conn + stop chan struct{} + pingFailHandler PingFailHandler + pingOutstanding int32 + debug Logger +} + +// DefaultPingerWithCustomFailHandler returns an instance of the +// default Pinger but with a custom PingFailHandler that is called +// when the client has not received a response to a PingRequest +// within the appropriate amount of time +func DefaultPingerWithCustomFailHandler(pfh PingFailHandler) *PingHandler { + return &PingHandler{ + pingFailHandler: pfh, + debug: NOOPLogger{}, + } +} + +// Start is the library provided Pinger's implementation of +// the required interface function() +func (p *PingHandler) Start(c net.Conn, pt time.Duration) { + p.mu.Lock() + p.conn = c + p.stop = make(chan struct{}) + p.mu.Unlock() + checkTicker := time.NewTicker(pt / 4) + defer checkTicker.Stop() + for { + select { + case <-p.stop: + return + case <-checkTicker.C: + if atomic.LoadInt32(&p.pingOutstanding) > 0 && time.Since(p.lastPing) > (pt+pt>>1) { + p.pingFailHandler(fmt.Errorf("ping resp timed out")) + //ping outstanding and not reset in 1.5 times ping timer + return + } + if time.Since(p.lastPing) >= pt { + //time to send a ping + if _, err := packets.NewControlPacket(packets.PINGREQ).WriteTo(p.conn); err != nil { + if p.pingFailHandler != nil { + p.pingFailHandler(err) + } + return + } + atomic.AddInt32(&p.pingOutstanding, 1) + p.lastPing = time.Now() + p.debug.Println("pingHandler sending ping request") + } + } + } +} + +// Stop is the library provided Pinger's implementation of +// the required interface function() +func (p *PingHandler) Stop() { + p.mu.Lock() + defer p.mu.Unlock() + if p.stop == nil { + return + } + p.debug.Println("pingHandler stopping") + select { + case <-p.stop: + //Already stopped, do nothing + default: + close(p.stop) + } +} + +// PingResp is the library provided Pinger's implementation of +// the required interface function() +func (p *PingHandler) PingResp() { + p.debug.Println("pingHandler resetting pingOutstanding") + atomic.StoreInt32(&p.pingOutstanding, 0) +} + +// SetDebug sets the logger l to be used for printing debug +// information for the pinger +func (p *PingHandler) SetDebug(l Logger) { + p.debug = l +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/router.go b/vendor/github.com/eclipse/paho.golang/paho/router.go new file mode 100644 index 00000000000..05031596f29 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/router.go @@ -0,0 +1,212 @@ +package paho + +import ( + "strings" + "sync" + + "github.com/eclipse/paho.golang/packets" +) + +// MessageHandler is a type for a function that is invoked +// by a Router when it has received a Publish. +type MessageHandler func(*Publish) + +// Router is an interface of the functions for a struct that is +// used to handle invoking MessageHandlers depending on the +// the topic the message was published on. +// RegisterHandler() takes a string of the topic, and a MessageHandler +// to be invoked when Publishes are received that match that topic +// UnregisterHandler() takes a string of the topic to remove +// MessageHandlers for +// Route() takes a Publish message and determines which MessageHandlers +// should be invoked +type Router interface { + RegisterHandler(string, MessageHandler) + UnregisterHandler(string) + Route(*packets.Publish) + SetDebugLogger(Logger) +} + +// StandardRouter is a library provided implementation of a Router that +// allows for unique and multiple MessageHandlers per topic +type StandardRouter struct { + sync.RWMutex + subscriptions map[string][]MessageHandler + aliases map[uint16]string + debug Logger +} + +// NewStandardRouter instantiates and returns an instance of a StandardRouter +func NewStandardRouter() *StandardRouter { + return &StandardRouter{ + subscriptions: make(map[string][]MessageHandler), + aliases: make(map[uint16]string), + debug: NOOPLogger{}, + } +} + +// RegisterHandler is the library provided StandardRouter's +// implementation of the required interface function() +func (r *StandardRouter) RegisterHandler(topic string, h MessageHandler) { + r.debug.Println("registering handler for:", topic) + r.Lock() + defer r.Unlock() + + r.subscriptions[topic] = append(r.subscriptions[topic], h) +} + +// UnregisterHandler is the library provided StandardRouter's +// implementation of the required interface function() +func (r *StandardRouter) UnregisterHandler(topic string) { + r.debug.Println("unregistering handler for:", topic) + r.Lock() + defer r.Unlock() + + delete(r.subscriptions, topic) +} + +// Route is the library provided StandardRouter's implementation +// of the required interface function() +func (r *StandardRouter) Route(pb *packets.Publish) { + r.debug.Println("routing message for:", pb.Topic) + r.RLock() + defer r.RUnlock() + + m := PublishFromPacketPublish(pb) + + var topic string + if pb.Properties.TopicAlias != nil { + r.debug.Println("message is using topic aliasing") + if pb.Topic != "" { + //Register new alias + r.debug.Printf("registering new topic alias '%d' for topic '%s'", *pb.Properties.TopicAlias, m.Topic) + r.aliases[*pb.Properties.TopicAlias] = pb.Topic + } + if t, ok := r.aliases[*pb.Properties.TopicAlias]; ok { + r.debug.Printf("aliased topic '%d' translates to '%s'", *pb.Properties.TopicAlias, m.Topic) + topic = t + } + } else { + topic = m.Topic + } + + for route, handlers := range r.subscriptions { + if match(route, topic) { + r.debug.Println("found handler for:", route) + for _, handler := range handlers { + handler(m) + } + } + } +} + +// SetDebugLogger sets the logger l to be used for printing debug +// information for the router +func (r *StandardRouter) SetDebugLogger(l Logger) { + r.debug = l +} + +func match(route, topic string) bool { + return route == topic || routeIncludesTopic(route, topic) +} + +func matchDeep(route []string, topic []string) bool { + if len(route) == 0 { + return len(topic) == 0 + } + + if len(topic) == 0 { + return route[0] == "#" + } + + if route[0] == "#" { + return true + } + + if (route[0] == "+") || (route[0] == topic[0]) { + return matchDeep(route[1:], topic[1:]) + } + return false +} + +func routeIncludesTopic(route, topic string) bool { + return matchDeep(routeSplit(route), topicSplit(topic)) +} + +func routeSplit(route string) []string { + if len(route) == 0 { + return nil + } + var result []string + if strings.HasPrefix(route, "$share") { + result = strings.Split(route, "/")[2:] + } else { + result = strings.Split(route, "/") + } + return result +} + +func topicSplit(topic string) []string { + if len(topic) == 0 { + return nil + } + return strings.Split(topic, "/") +} + +// SingleHandlerRouter is a library provided implementation of a Router +// that stores only a single MessageHandler and invokes this MessageHandler +// for all received Publishes +type SingleHandlerRouter struct { + sync.Mutex + aliases map[uint16]string + handler MessageHandler + debug Logger +} + +// NewSingleHandlerRouter instantiates and returns an instance of a SingleHandlerRouter +func NewSingleHandlerRouter(h MessageHandler) *SingleHandlerRouter { + return &SingleHandlerRouter{ + aliases: make(map[uint16]string), + handler: h, + debug: NOOPLogger{}, + } +} + +// RegisterHandler is the library provided SingleHandlerRouter's +// implementation of the required interface function() +func (s *SingleHandlerRouter) RegisterHandler(topic string, h MessageHandler) { + s.debug.Println("registering handler for:", topic) + s.handler = h +} + +// UnregisterHandler is the library provided SingleHandlerRouter's +// implementation of the required interface function() +func (s *SingleHandlerRouter) UnregisterHandler(topic string) {} + +// Route is the library provided SingleHandlerRouter's +// implementation of the required interface function() +func (s *SingleHandlerRouter) Route(pb *packets.Publish) { + m := PublishFromPacketPublish(pb) + + s.debug.Println("routing message for:", m.Topic) + + if pb.Properties.TopicAlias != nil { + s.debug.Println("message is using topic aliasing") + if pb.Topic != "" { + //Register new alias + s.debug.Printf("registering new topic alias '%d' for topic '%s'", *pb.Properties.TopicAlias, m.Topic) + s.aliases[*pb.Properties.TopicAlias] = pb.Topic + } + if t, ok := s.aliases[*pb.Properties.TopicAlias]; ok { + s.debug.Printf("aliased topic '%d' translates to '%s'", *pb.Properties.TopicAlias, m.Topic) + m.Topic = t + } + } + s.handler(m) +} + +// SetDebugLogger sets the logger l to be used for printing debug +// information for the router +func (s *SingleHandlerRouter) SetDebugLogger(l Logger) { + s.debug = l +} diff --git a/vendor/github.com/eclipse/paho.golang/paho/trace.go b/vendor/github.com/eclipse/paho.golang/paho/trace.go new file mode 100644 index 00000000000..586c9239891 --- /dev/null +++ b/vendor/github.com/eclipse/paho.golang/paho/trace.go @@ -0,0 +1,22 @@ +package paho + +type ( + // Logger interface allows implementations to provide to this package any + // object that implements the methods defined in it. + Logger interface { + Println(v ...interface{}) + Printf(format string, v ...interface{}) + } + + // NOOPLogger implements the logger that does not perform any operation + // by default. This allows us to efficiently discard the unwanted messages. + NOOPLogger struct{} +) + +// Println is the library provided NOOPLogger's +// implementation of the required interface function() +func (NOOPLogger) Println(v ...interface{}) {} + +// Printf is the library provided NOOPLogger's +// implementation of the required interface function(){} +func (NOOPLogger) Printf(format string, v ...interface{}) {} diff --git a/vendor/modules.txt b/vendor/modules.txt index 02fafffd67b..0d2b57bb6c7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -80,6 +80,9 @@ github.com/cloudevents/conformance/pkg/http ## explicit; go 1.18 github.com/cloudevents/sdk-go/observability/opencensus/v2/client github.com/cloudevents/sdk-go/observability/opencensus/v2/http +# github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 v2.0.0-20240508060731-1ed9471c98bd +## explicit; go 1.18 +github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 # github.com/cloudevents/sdk-go/sql/v2 v2.15.2 ## explicit; go 1.18 github.com/cloudevents/sdk-go/sql/v2 @@ -116,6 +119,10 @@ github.com/coreos/go-oidc/v3/oidc # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew +# github.com/eclipse/paho.golang v0.12.0 +## explicit; go 1.20 +github.com/eclipse/paho.golang/packets +github.com/eclipse/paho.golang/paho # github.com/emicklei/go-restful/v3 v3.11.0 ## explicit; go 1.13 github.com/emicklei/go-restful/v3