forked from jshiffer/go-xmpp
Compare commits
No commits in common. "7ec2b8b7def69491528689aeb58253e9421a4b73" and "7186c058fd8eb4985efa52ed00ddbef30371c211" have entirely different histories.
7ec2b8b7de
...
7186c058fd
38
.github/workflows/test.yaml
vendored
Normal file
38
.github/workflows/test.yaml
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
name: Run tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- .github/workflows/test.yaml
|
||||||
|
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- .github/workflows/test.yaml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.13
|
||||||
|
uses: actions/setup-go@v1
|
||||||
|
with:
|
||||||
|
go-version: 1.13
|
||||||
|
id: go
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
go test ./... -v -race -coverprofile cover.out -covermode=atomic
|
||||||
|
- name: Convert coverage to lcov
|
||||||
|
uses: jandelgado/gcov2lcov-action@v1
|
||||||
|
with:
|
||||||
|
infile: cover.out
|
||||||
|
outfile: coverage.lcov
|
||||||
|
- name: Coveralls
|
||||||
|
uses: coverallsapp/github-action@v1
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.github_token }}
|
||||||
|
path-to-lcov: coverage.lcov
|
36
.gitignore
vendored
Executable file
36
.gitignore
vendored
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
coverage.out
|
||||||
|
coverage.txt
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Do not commit codeship key
|
||||||
|
codeship.aes
|
||||||
|
codeship.env
|
||||||
|
|
||||||
|
priv/
|
@ -1,5 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- tip
|
|
||||||
script:
|
|
||||||
- go test
|
|
58
CHANGELOG.md
Normal file
58
CHANGELOG.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Fluux XMPP Changelog
|
||||||
|
|
||||||
|
## v0.5.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Added support for XEP-0198 (Stream management)
|
||||||
|
- Added message queue : when using "SendX" methods on a client, messages are also stored in a queue. When requesting
|
||||||
|
acks from the server, sent messages will be discarded, and unsent ones will be sent again. (see https://xmpp.org/extensions/xep-0198.html#acking)
|
||||||
|
- Added support for stanza_errors (see https://xmpp.org/rfcs/rfc3920.html#def C.2. Stream error namespace and https://xmpp.org/rfcs/rfc6120.html#schemas-streamerror)
|
||||||
|
- Added separate hooks for connection and reconnection on the client. One can now specify different actions to get triggered on client connect
|
||||||
|
and reconnect, at client init time.
|
||||||
|
- Client state update is now thread safe
|
||||||
|
- Changed the Config struct to use pointer semantics
|
||||||
|
- Tests
|
||||||
|
- Refactoring, including removing some Fprintf statements in favor of Marshal + Write and using structs from the library
|
||||||
|
instead of strings
|
||||||
|
|
||||||
|
## v0.4.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Added support for XEP-0060 (PubSub)
|
||||||
|
(no support for 6.5.4 Returning Some Items yet as it needs XEP-0059, Result Sets)
|
||||||
|
- Added support for XEP-0050 (Commands)
|
||||||
|
- Added support for XEP-0004 (Forms)
|
||||||
|
- Updated the client example with a TUI
|
||||||
|
- Make keepalive interval configurable #134
|
||||||
|
- Fix updating of EventManager.CurrentState #136
|
||||||
|
- Added callbacks for error management in Component and Client. Users must now provide a callback function when using NewClient/Component.
|
||||||
|
- Moved JID from xmpp package to stanza package
|
||||||
|
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Update requirements to go1.13
|
||||||
|
- Add a websocket transport
|
||||||
|
- Add Client.SendIQ method
|
||||||
|
- Add IQ result routes to the Router
|
||||||
|
- Fix SIGSEGV in xmpp_component (#126)
|
||||||
|
- Add tests for Component and code style fixes
|
||||||
|
|
||||||
|
## v0.2.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- XMPP Over Websocket support
|
||||||
|
- Add support for getting IQ responses to client IQ queries (synchronously or asynchronously, passing an handler
|
||||||
|
function).
|
||||||
|
- Implement X-OAUTH2 authentication method. You can read more details here:
|
||||||
|
[Understanding ejabberd OAuth Support & Roadmap: Step 4](https://blog.process-one.net/understanding-ejabberd-oauth-support-roadmap/)
|
||||||
|
- Fix issues in the stanza builder when trying to add text inside and XMPP node.
|
||||||
|
- Fix issues with unescaped % characters in XMPP payload.
|
||||||
|
|
||||||
|
### Code migration guide
|
||||||
|
|
||||||
|
TODO
|
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at contact@process-one.net. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
139
CONTRIBUTING.md
Normal file
139
CONTRIBUTING.md
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
We'd love for you to contribute to our source code and to make our project even better than it is
|
||||||
|
today! Here are the guidelines we'd like you to follow:
|
||||||
|
|
||||||
|
* [Code of Conduct](#coc)
|
||||||
|
* [Questions and Problems](#question)
|
||||||
|
* [Issues and Bugs](#issue)
|
||||||
|
* [Feature Requests](#feature)
|
||||||
|
* [Issue Submission Guidelines](#submit)
|
||||||
|
* [Pull Request Submission Guidelines](#submit-pr)
|
||||||
|
* [Signing the CLA](#cla)
|
||||||
|
|
||||||
|
## <a name="coc"></a> Code of Conduct
|
||||||
|
|
||||||
|
Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc].
|
||||||
|
|
||||||
|
## <a name="requests"></a> Questions, Bugs, Features
|
||||||
|
|
||||||
|
### <a name="question"></a> Got a Question or Problem?
|
||||||
|
|
||||||
|
Do not open issues for general support questions as we want to keep GitHub issues for bug reports
|
||||||
|
and feature requests. You've got much better chances of getting your question answered on dedicated
|
||||||
|
support platforms, the best being [Stack Overflow][stackoverflow].
|
||||||
|
|
||||||
|
Stack Overflow is a much better place to ask questions since:
|
||||||
|
|
||||||
|
- there are thousands of people willing to help on Stack Overflow
|
||||||
|
- questions and answers stay available for public viewing so your question / answer might help
|
||||||
|
someone else
|
||||||
|
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||||
|
|
||||||
|
To save your and our time, we will systematically close all issues that are requests for general
|
||||||
|
support and redirect people to the section you are reading right now.
|
||||||
|
|
||||||
|
### <a name="issue"></a> Found an Issue or Bug?
|
||||||
|
|
||||||
|
If you find a bug in the source code, you can help us by submitting an issue to our
|
||||||
|
[GitHub Repository][github]. Even better, you can submit a Pull Request with a fix.
|
||||||
|
|
||||||
|
### <a name="feature"></a> Missing a Feature?
|
||||||
|
|
||||||
|
You can request a new feature by submitting an issue to our [GitHub Repository][github-issues].
|
||||||
|
|
||||||
|
If you would like to implement a new feature then consider what kind of change it is:
|
||||||
|
|
||||||
|
* **Major Changes** that you wish to contribute to the project should be discussed first in an
|
||||||
|
[GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature.
|
||||||
|
* **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github]
|
||||||
|
as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr).
|
||||||
|
|
||||||
|
## <a name="submit"></a> Issue Submission Guidelines
|
||||||
|
|
||||||
|
Before you submit your issue search the archive, maybe your question was already answered.
|
||||||
|
|
||||||
|
If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize
|
||||||
|
the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
|
||||||
|
|
||||||
|
The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to
|
||||||
|
make it easier to understand and categorize the issue.
|
||||||
|
|
||||||
|
## <a name="submit-pr"></a> Pull Request Submission Guidelines
|
||||||
|
|
||||||
|
By submitting a pull request for a code or doc contribution, you need to have the right
|
||||||
|
to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla]
|
||||||
|
for details.
|
||||||
|
|
||||||
|
Before you submit your pull request consider the following guidelines:
|
||||||
|
|
||||||
|
* Search [GitHub][github-pr] for an open or closed Pull Request
|
||||||
|
that relates to your submission. You don't want to duplicate effort.
|
||||||
|
* Make your changes in a new git branch:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git checkout -b my-fix-branch master
|
||||||
|
```
|
||||||
|
* Test your changes and, if relevant, expand the automated test suite.
|
||||||
|
* Create your patch commit, including appropriate test cases.
|
||||||
|
* If the changes affect public APIs, change or add relevant documentation.
|
||||||
|
* Commit your changes using a descriptive commit message.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git commit -a
|
||||||
|
```
|
||||||
|
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||||
|
|
||||||
|
* Push your branch to GitHub:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git push origin my-fix-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
* In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test.
|
||||||
|
We will also notify you if you have not yet signed the [contribution agreement][cla].
|
||||||
|
|
||||||
|
* If you find that the continunous integration has failed, look into the logs to find out
|
||||||
|
if your changes caused test failures, the commit message was malformed etc. If you find that the
|
||||||
|
tests failed or times out for unrelated reasons, you can ping a team member so that the build can be
|
||||||
|
restarted.
|
||||||
|
|
||||||
|
* If we suggest changes, then:
|
||||||
|
|
||||||
|
* Make the required updates.
|
||||||
|
* Test your changes and test cases.
|
||||||
|
* Commit your changes to your branch (e.g. `my-fix-branch`).
|
||||||
|
* Push the changes to your GitHub repository (this will update your Pull Request).
|
||||||
|
|
||||||
|
You can also amend the initial commits and force push them to the branch.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git rebase master -i
|
||||||
|
git push origin my-fix-branch -f
|
||||||
|
```
|
||||||
|
|
||||||
|
This is generally easier to follow, but separate commits are useful if the Pull Request contains
|
||||||
|
iterations that might be interesting to see side-by-side.
|
||||||
|
|
||||||
|
That's it! Thank you for your contribution!
|
||||||
|
|
||||||
|
## <a name="cla"></a> Signing the Contributor License Agreement (CLA)
|
||||||
|
|
||||||
|
Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done
|
||||||
|
so before. It's a quick process, we promise, and you will be able to do it all online
|
||||||
|
|
||||||
|
You can read [ProcessOne Contribution License Agreement][cla] in PDF.
|
||||||
|
|
||||||
|
This is part of the legal framework of the open-source ecosystem that adds some red tape,
|
||||||
|
but protects both the contributor and the company / foundation behind the project. It also
|
||||||
|
gives us the option to relicense the code with a more permissive license in the future.
|
||||||
|
|
||||||
|
|
||||||
|
[coc]: https://github.com/FluuxIO/go-xmpp/blob/master/CODE_OF_CONDUCT.md
|
||||||
|
[stackoverflow]: https://stackoverflow.com/
|
||||||
|
[github]: https://github.com/FluuxIO/go-xmpp
|
||||||
|
[github-issues]: https://github.com/FluuxIO/go-xmpp/issues
|
||||||
|
[github-new-issue]: https://github.com/FluuxIO/go-xmpp/issues/new
|
||||||
|
[github-pr]: https://github.com/FluuxIO/go-xmpp/pulls
|
||||||
|
[cla]: https://www.process-one.net/resources/ejabberd-cla.pdf
|
||||||
|
[license]: https://github.com/FluuxIO/go-xmpp/blob/master/LICENSE
|
42
LICENSE
42
LICENSE
@ -1,27 +1,29 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2017, ProcessOne SARL
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are met:
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
notice, this list of conditions and the following disclaimer.
|
list of conditions and the following disclaimer.
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
in the documentation and/or other materials provided with the
|
this list of conditions and the following disclaimer in the documentation
|
||||||
distribution.
|
and/or other materials provided with the distribution.
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
160
README.md
160
README.md
@ -1,6 +1,158 @@
|
|||||||
go-xmpp
|
# Fluux XMPP
|
||||||
=======
|
|
||||||
|
|
||||||
go xmpp library (original was written by russ cox )
|
[![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![Coverage Status](https://coveralls.io/repos/github/FluuxIO/go-xmpp/badge.svg?branch=master)](https://coveralls.io/github/FluuxIO/go-xmpp?branch=master)
|
||||||
|
|
||||||
[Documentation](https://godoc.org/github.com/mattn/go-xmpp)
|
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
||||||
|
|
||||||
|
The goal is to make simple to write simple XMPP clients and components:
|
||||||
|
|
||||||
|
- For automation (like for example monitoring of an XMPP service),
|
||||||
|
- For building connected "things" by plugging them on an XMPP server,
|
||||||
|
- For writing simple chatbot to control a service or a thing,
|
||||||
|
- For writing XMPP servers components.
|
||||||
|
|
||||||
|
The library is designed to have minimal dependencies. Currently it requires at least Go 1.13.
|
||||||
|
|
||||||
|
## Configuration and connection
|
||||||
|
|
||||||
|
### Allowing Insecure TLS connection during development
|
||||||
|
|
||||||
|
It is not recommended to disable the check for domain name and certificate chain. Doing so would open your client
|
||||||
|
to man-in-the-middle attacks.
|
||||||
|
|
||||||
|
However, in development, XMPP servers often use self-signed certificates. In that situation, it is better to add the
|
||||||
|
root CA that signed the certificate to your trusted list of root CA. It avoids changing the code and limit the risk
|
||||||
|
of shipping an insecure client to production.
|
||||||
|
|
||||||
|
That said, if you really want to allow your client to trust any TLS certificate, you can customize Go standard
|
||||||
|
`tls.Config` and set it in Config struct.
|
||||||
|
|
||||||
|
Here is an example code to configure a client to allow connecting to a server with self-signed certificate. Note the
|
||||||
|
`InsecureSkipVerify` option. When using this `tls.Config` option, all the checks on the certificate are skipped.
|
||||||
|
|
||||||
|
```go
|
||||||
|
config := xmpp.Config{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("Test"),
|
||||||
|
TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported specifications
|
||||||
|
|
||||||
|
### Clients
|
||||||
|
|
||||||
|
- [RFC 6120: XMPP Core](https://xmpp.org/rfcs/rfc6120.html)
|
||||||
|
- [RFC 6121: XMPP Instant Messaging and Presence](https://xmpp.org/rfcs/rfc6121.html)
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- [XEP-0114: Jabber Component Protocol](https://xmpp.org/extensions/xep-0114.html)
|
||||||
|
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
|
||||||
|
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
- [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
|
||||||
|
Note : "6.5.4 Returning Some Items" requires support for [XEP-0059: Result Set Management](https://xmpp.org/extensions/xep-0059.html),
|
||||||
|
and is therefore not supported yet.
|
||||||
|
- [XEP-0004: Data Forms](https://xmpp.org/extensions/xep-0004.html)
|
||||||
|
- [XEP-0050: Ad-Hoc Commands](https://xmpp.org/extensions/xep-0050.html)
|
||||||
|
|
||||||
|
## Package overview
|
||||||
|
|
||||||
|
### Stanza subpackage
|
||||||
|
|
||||||
|
XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to
|
||||||
|
leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas
|
||||||
|
back and forth.
|
||||||
|
|
||||||
|
At a low-level, stanzas are XML fragments. However, Fluux XMPP library provides the building blocks to interact with
|
||||||
|
stanzas at a high-level, providing a Go-friendly API.
|
||||||
|
|
||||||
|
The `stanza` subpackage provides support for XMPP stream parsing, marshalling and unmarshalling of XMPP stanza. It is a
|
||||||
|
bridge between high-level Go structure and low-level XMPP protocol.
|
||||||
|
|
||||||
|
Parsing, marshalling and unmarshalling is automatically handled by Fluux XMPP client library. As a developer, you will
|
||||||
|
generally manipulates only the high-level structs provided by the stanza package.
|
||||||
|
|
||||||
|
The XMPP protocol, as the name implies is extensible. If your application is using custom stanza extensions, you can
|
||||||
|
implement your own extensions directly in your own application.
|
||||||
|
|
||||||
|
To learn more about the stanza package, you can read more in the
|
||||||
|
[stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md).
|
||||||
|
|
||||||
|
### Router
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Getting IQ response from server
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using
|
||||||
|
Fluux XMPP library.
|
||||||
|
|
||||||
|
Here is the demo "echo" client:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("test"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
|
_ = s.Send(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference documentation
|
||||||
|
|
||||||
|
The code documentation is available on GoDoc: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
|
||||||
|
@ -1,112 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"github.com/mattn/go-gtk/gtk"
|
|
||||||
"github.com/mattn/go-xmpp"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
gtk.Init(&os.Args)
|
|
||||||
|
|
||||||
window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL)
|
|
||||||
window.SetTitle("GoTalk")
|
|
||||||
window.Connect("destroy", func() {
|
|
||||||
gtk.MainQuit()
|
|
||||||
})
|
|
||||||
vbox := gtk.NewVBox(false, 1)
|
|
||||||
scrolledwin := gtk.NewScrolledWindow(nil, nil)
|
|
||||||
textview := gtk.NewTextView()
|
|
||||||
textview.SetEditable(false)
|
|
||||||
textview.SetCursorVisible(false)
|
|
||||||
scrolledwin.Add(textview)
|
|
||||||
vbox.Add(scrolledwin)
|
|
||||||
|
|
||||||
buffer := textview.GetBuffer()
|
|
||||||
|
|
||||||
entry := gtk.NewEntry()
|
|
||||||
vbox.PackEnd(entry, false, false, 0)
|
|
||||||
|
|
||||||
window.Add(vbox)
|
|
||||||
window.SetSizeRequest(300, 400)
|
|
||||||
window.ShowAll()
|
|
||||||
|
|
||||||
dialog := gtk.NewDialog()
|
|
||||||
dialog.SetTitle(window.GetTitle())
|
|
||||||
sgroup := gtk.NewSizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
|
|
||||||
|
|
||||||
hbox := gtk.NewHBox(false, 1)
|
|
||||||
dialog.GetVBox().Add(hbox)
|
|
||||||
label := gtk.NewLabel("username:")
|
|
||||||
sgroup.AddWidget(label)
|
|
||||||
hbox.Add(label)
|
|
||||||
username := gtk.NewEntry()
|
|
||||||
hbox.Add(username)
|
|
||||||
|
|
||||||
hbox = gtk.NewHBox(false, 1)
|
|
||||||
dialog.GetVBox().Add(hbox)
|
|
||||||
label = gtk.NewLabel("password:")
|
|
||||||
sgroup.AddWidget(label)
|
|
||||||
hbox.Add(label)
|
|
||||||
password := gtk.NewEntry()
|
|
||||||
password.SetVisibility(false)
|
|
||||||
hbox.Add(password)
|
|
||||||
|
|
||||||
dialog.AddButton(gtk.STOCK_OK, gtk.RESPONSE_OK)
|
|
||||||
dialog.AddButton(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
|
||||||
dialog.SetDefaultResponse(gtk.RESPONSE_OK)
|
|
||||||
dialog.SetTransientFor(window)
|
|
||||||
dialog.ShowAll()
|
|
||||||
res := dialog.Run()
|
|
||||||
username_ := username.GetText()
|
|
||||||
password_ := password.GetText()
|
|
||||||
dialog.Destroy()
|
|
||||||
if res != gtk.RESPONSE_OK {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
xmpp.DefaultConfig = tls.Config{
|
|
||||||
ServerName: "talk.google.com",
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
talk, err := xmpp.NewClient("talk.google.com:443", username_, password_, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.Connect("activate", func() {
|
|
||||||
text := entry.GetText()
|
|
||||||
tokens := strings.SplitN(text, " ", 2)
|
|
||||||
if len(tokens) == 2 {
|
|
||||||
func() {
|
|
||||||
defer recover()
|
|
||||||
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
|
|
||||||
entry.SetText("")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
func() {
|
|
||||||
defer recover()
|
|
||||||
chat, err := talk.Recv()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var iter gtk.TextIter
|
|
||||||
buffer.GetStartIter(&iter)
|
|
||||||
if msg, ok := chat.(xmpp.Chat); ok {
|
|
||||||
buffer.Insert(&iter, msg.Remote+": "+msg.Text+"\n")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
gtk.Main()
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"crypto/tls"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/mattn/go-xmpp"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var server = flag.String("server", "talk.google.com:443", "server")
|
|
||||||
var username = flag.String("username", "", "username")
|
|
||||||
var password = flag.String("password", "", "password")
|
|
||||||
var status = flag.String("status", "xa", "status")
|
|
||||||
var statusMessage = flag.String("status-msg", "I for one welcome our new codebot overlords.", "status message")
|
|
||||||
var notls = flag.Bool("notls", false, "No TLS")
|
|
||||||
var debug = flag.Bool("debug", false, "debug output")
|
|
||||||
var session = flag.Bool("session", false, "use server session")
|
|
||||||
|
|
||||||
func serverName(host string) string {
|
|
||||||
return strings.Split(host, ":")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Usage = func() {
|
|
||||||
fmt.Fprintf(os.Stderr, "usage: example [options]\n")
|
|
||||||
flag.PrintDefaults()
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
flag.Parse()
|
|
||||||
if *username == "" || *password == "" {
|
|
||||||
if *debug && *username == "" && *password == "" {
|
|
||||||
fmt.Fprintf(os.Stderr, "no username or password were given; attempting ANONYMOUS auth\n")
|
|
||||||
} else if *username != "" || *password != "" {
|
|
||||||
flag.Usage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*notls {
|
|
||||||
xmpp.DefaultConfig = tls.Config{
|
|
||||||
ServerName: serverName(*server),
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var talk *xmpp.Client
|
|
||||||
var err error
|
|
||||||
options := xmpp.Options{Host: *server,
|
|
||||||
User: *username,
|
|
||||||
Password: *password,
|
|
||||||
NoTLS: *notls,
|
|
||||||
Debug: *debug,
|
|
||||||
Session: *session,
|
|
||||||
Status: *status,
|
|
||||||
StatusMessage: *statusMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
talk, err = options.NewClient()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
chat, err := talk.Recv()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
switch v := chat.(type) {
|
|
||||||
case xmpp.Chat:
|
|
||||||
fmt.Println(v.Remote, v.Text)
|
|
||||||
case xmpp.Presence:
|
|
||||||
fmt.Println(v.From, v.Show)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
in := bufio.NewReader(os.Stdin)
|
|
||||||
line, err := in.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
line = strings.TrimRight(line, "\n")
|
|
||||||
|
|
||||||
tokens := strings.SplitN(line, " ", 2)
|
|
||||||
if len(tokens) == 2 {
|
|
||||||
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
5
_examples/custom_stanza/README.md
Normal file
5
_examples/custom_stanza/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Custom Stanza example
|
||||||
|
|
||||||
|
This module show how to implement a custom extension for your own client, without having to modify or fork Fluux XMPP.
|
||||||
|
|
||||||
|
It help integrating your custom extension in the standard stream parsing, marshalling and unmarshalling workflow.
|
55
_examples/custom_stanza/custom_stanza.go
Normal file
55
_examples/custom_stanza/custom_stanza.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
payload := CustomPayload{XMLName: xml.Name{Space: "my:custom:payload", Local: "query"}, Node: "test"}
|
||||||
|
iq.Payload = payload
|
||||||
|
|
||||||
|
data, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Cannot marshal iq with custom payload: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedIQ stanza.IQ
|
||||||
|
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||||
|
log.Fatalf("Cannot unmarshal(%s): %s", data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedPayload, ok := parsedIQ.Payload.(*CustomPayload)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("Incorrect payload type: %#v", parsedIQ.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Parsed Payload: %#v", parsedPayload)
|
||||||
|
|
||||||
|
if parsedPayload.Node != "test" {
|
||||||
|
log.Fatalf("Incorrect node value: %s", parsedPayload.Node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomPayload struct {
|
||||||
|
XMLName xml.Name `xml:"my:custom:payload query"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomPayload) Namespace() string {
|
||||||
|
return c.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomPayload) GetSet() *stanza.ResultSet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func init() {
|
||||||
|
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "my:custom:payload", Local: "query"}, CustomPayload{})
|
||||||
|
}
|
5
_examples/delegation/README.md
Normal file
5
_examples/delegation/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Advanced component: delegation
|
||||||
|
|
||||||
|
`delegation` is an example of advanced component supporting Namespace Delegation
|
||||||
|
([XEP-0355](https://xmpp.org/extensions/xep-0355.html)) and privileged entity
|
||||||
|
([XEP-356](https://xmpp.org/extensions/xep-0356.html)).
|
220
_examples/delegation/delegation.go
Normal file
220
_examples/delegation/delegation.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:9999",
|
||||||
|
Domain: "service.localhost",
|
||||||
|
},
|
||||||
|
Domain: "service.localhost",
|
||||||
|
Secret: "mypass",
|
||||||
|
|
||||||
|
// TODO: Move that part to a component discovery handler
|
||||||
|
Name: "Test Component",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces(stanza.NSDiscoInfo).
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
discoInfo(s, p, opts)
|
||||||
|
})
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces("urn:xmpp:delegation:1").
|
||||||
|
HandlerFunc(handleDelegation)
|
||||||
|
|
||||||
|
component, err := xmpp.NewComponent(opts, router, func(err error) {
|
||||||
|
log.Println(err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the component to a stream manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
|
||||||
|
cm := xmpp.NewStreamManager(component, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msgProcessed bool
|
||||||
|
for _, ext := range msg.Extensions {
|
||||||
|
delegation, ok := ext.(*stanza.Delegation)
|
||||||
|
if ok {
|
||||||
|
msgProcessed = true
|
||||||
|
fmt.Printf("Delegation confirmed for namespace %s\n", delegation.Delegated.Namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Decode privilege message
|
||||||
|
// <message to='service.localhost' from='localhost'><privilege xmlns='urn:xmpp:privilege:1'><perm type='outgoing' access='message'/><perm type='get' access='roster'/><perm type='managed_entity' access='presence'/></privilege></message>
|
||||||
|
|
||||||
|
if !msgProcessed {
|
||||||
|
fmt.Printf("Ignored received message, not related to delegation: %v\n", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
pubsubNode = "urn:xmpp:delegation:1::http://jabber.org/protocol/pubsub"
|
||||||
|
pepNode = "urn:xmpp:delegation:1:bare:http://jabber.org/protocol/pubsub"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: replace xmpp.Sender by ctx xmpp.Context ?
|
||||||
|
// ctx.Stream.Send / SendRaw
|
||||||
|
// ctx.Opts
|
||||||
|
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, ok := iq.Payload.(*stanza.DiscoInfo)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create IQ response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch info.Node {
|
||||||
|
case "":
|
||||||
|
discoInfoRoot(iqResp, opts)
|
||||||
|
case pubsubNode:
|
||||||
|
discoInfoPubSub(iqResp)
|
||||||
|
case pepNode:
|
||||||
|
discoInfoPEP(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
|
||||||
|
disco := iqResp.DiscoInfo()
|
||||||
|
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||||
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfoPubSub(iqResp *stanza.IQ) {
|
||||||
|
payload := stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: stanza.NSDiscoInfo,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
Node: pubsubNode,
|
||||||
|
Features: []stanza.Feature{
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#publish"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#publish-options"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqResp.Payload = &payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfoPEP(iqResp *stanza.IQ) {
|
||||||
|
identity := stanza.Identity{
|
||||||
|
Category: "pubsub",
|
||||||
|
Type: "pep",
|
||||||
|
}
|
||||||
|
payload := stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: stanza.NSDiscoInfo,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
Identity: []stanza.Identity{identity},
|
||||||
|
Node: pepNode,
|
||||||
|
Features: []stanza.Feature{
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#access-presence"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#auto-create"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#auto-subscribe"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#config-node"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#create-and-configure"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#create-nodes"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#filtered-notifications"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#persistent-items"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#publish"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#retrieve-items"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqResp.Payload = &payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delegation, ok := iq.Payload.(*stanza.Delegation)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forwardedPacket := delegation.Forwarded.Stanza
|
||||||
|
fmt.Println(forwardedPacket)
|
||||||
|
forwardedIQ, ok := forwardedPacket.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
// We only support pubsub delegation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Publish.XMLName.Local == "publish" {
|
||||||
|
// Prepare pubsub IQ reply
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create iqResp: %v", err)
|
||||||
|
}
|
||||||
|
payload := stanza.PubSubGeneric{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://jabber.org/protocol/pubsub",
|
||||||
|
Local: "pubsub",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqResp.Payload = &payload
|
||||||
|
// Wrap the reply in delegation 'forward'
|
||||||
|
iqForward, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create iqForward: %v", err)
|
||||||
|
}
|
||||||
|
delegPayload := stanza.Delegation{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "urn:xmpp:delegation:1",
|
||||||
|
Local: "delegation",
|
||||||
|
},
|
||||||
|
Forwarded: &stanza.Forwarded{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "urn:xmpp:forward:0",
|
||||||
|
Local: "forward",
|
||||||
|
},
|
||||||
|
Stanza: iqResp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqForward.Payload = &delegPayload
|
||||||
|
_ = s.Send(iqForward)
|
||||||
|
// TODO: The component should actually broadcast the mood to subscribers
|
||||||
|
}
|
||||||
|
}
|
11
_examples/go.mod
Normal file
11
_examples/go.mod
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module gosrc.io/xmpp/_examples
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/processone/mpg123 v1.0.0
|
||||||
|
github.com/processone/soundcloud v1.0.0
|
||||||
|
gosrc.io/xmpp v0.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
replace gosrc.io/xmpp => ./../
|
214
_examples/go.sum
Normal file
214
_examples/go.sum
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
|
||||||
|
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA=
|
||||||
|
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
||||||
|
github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE=
|
||||||
|
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
3
_examples/muc_bot/README.md
Normal file
3
_examples/muc_bot/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# XMPP Multi-User (MUC) chat bot example
|
||||||
|
|
||||||
|
This code shows how to build a simple basic XMPP Multi-User chat bot using Fluux Go XMPP library.
|
51
_examples/xmpp_chat_client/README.md
Normal file
51
_examples/xmpp_chat_client/README.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Chat TUI example
|
||||||
|
This is a simple chat example, with a TUI.
|
||||||
|
It shows the library usage and a few of its capabilities.
|
||||||
|
## How to run
|
||||||
|
### Build
|
||||||
|
You can build the client using :
|
||||||
|
```
|
||||||
|
go build -o example_client
|
||||||
|
```
|
||||||
|
and then run with (on unix for example):
|
||||||
|
```
|
||||||
|
./example_client
|
||||||
|
```
|
||||||
|
or you can simply build + run in one command while at the example directory root, like this:
|
||||||
|
```
|
||||||
|
go run xmpp_chat_client.go interface.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
The example needs a configuration file to run. A sample file is provided.
|
||||||
|
By default, the example will look for a file named "config" in the current directory.
|
||||||
|
To provide a different configuration file, pass the following argument to the example :
|
||||||
|
```
|
||||||
|
go run xmpp_chat_client.go interface.go -c /path/to/config
|
||||||
|
```
|
||||||
|
where /path/to/config is the path to the directory containing the configuration file. The configuration file must be named
|
||||||
|
"config" and be using the yaml format.
|
||||||
|
|
||||||
|
Required fields are :
|
||||||
|
```yaml
|
||||||
|
Server :
|
||||||
|
- full_address: "localhost:5222"
|
||||||
|
Client : # This is you
|
||||||
|
- jid: "testuser2@localhost"
|
||||||
|
- pass: "pass123" #Password in a config file yay
|
||||||
|
|
||||||
|
# Contacts list, ";" separated
|
||||||
|
Contacts : "testuser1@localhost;testuser3@localhost"
|
||||||
|
# Should we log stanzas ?
|
||||||
|
LogStanzas:
|
||||||
|
- logger_on: "true"
|
||||||
|
- logfile_path: "./logs" # Path to directory, not file.
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
Shortcuts :
|
||||||
|
- ctrl+space : switch between input window and menu window.
|
||||||
|
- While in input window :
|
||||||
|
- enter : sends a message if in message mode (see menu options)
|
||||||
|
- ctrl+e : sends a raw stanza when in raw mode (see menu options)
|
||||||
|
- ctrl+c : quit
|
13
_examples/xmpp_chat_client/config.yml
Normal file
13
_examples/xmpp_chat_client/config.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Sample config for the client
|
||||||
|
Server :
|
||||||
|
- full_address: "localhost:5222"
|
||||||
|
Client :
|
||||||
|
- jid: "testuser2@localhost"
|
||||||
|
- pass: "pass123" #Password in a config file yay
|
||||||
|
|
||||||
|
Contacts : "testuser1@localhost;testuser3@localhost"
|
||||||
|
|
||||||
|
LogStanzas:
|
||||||
|
- logger_on: "true"
|
||||||
|
- logfile_path: "./logs"
|
||||||
|
|
10
_examples/xmpp_chat_client/go.mod
Normal file
10
_examples/xmpp_chat_client/go.mod
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module go-xmpp/_examples/xmpp_chat_client
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
github.com/spf13/viper v1.6.1
|
||||||
|
gosrc.io/xmpp v0.4.0
|
||||||
|
)
|
371
_examples/xmpp_chat_client/interface.go
Normal file
371
_examples/xmpp_chat_client/interface.go
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/awesome-gocui/gocui"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Windows
|
||||||
|
chatLogWindow = "clw" // Where (received and sent) messages are logged
|
||||||
|
chatInputWindow = "iw" // Where messages are written
|
||||||
|
rawInputWindow = "rw" // Where raw stanzas are written
|
||||||
|
contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable
|
||||||
|
menuWindow = "mw" // Where the menu is shown
|
||||||
|
disconnectMsg = "msg"
|
||||||
|
|
||||||
|
// Windows titles
|
||||||
|
chatLogWindowTitle = "Chat log"
|
||||||
|
menuWindowTitle = "Menu"
|
||||||
|
chatInputWindowTitle = "Write a message :"
|
||||||
|
rawInputWindowTitle = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :"
|
||||||
|
contactsListWindowTitle = "Contacts"
|
||||||
|
|
||||||
|
// Menu options
|
||||||
|
disconnect = "Disconnect"
|
||||||
|
askServerForRoster = "Ask server for roster"
|
||||||
|
rawMode = "Switch to Send Raw Mode"
|
||||||
|
messageMode = "Switch to Send Message Mode"
|
||||||
|
contactList = "Contacts list"
|
||||||
|
backFromContacts = "<- Go back"
|
||||||
|
)
|
||||||
|
|
||||||
|
// To store names of views on top
|
||||||
|
type viewsState struct {
|
||||||
|
input string // Which input view is on top
|
||||||
|
side string // Which side view is on top
|
||||||
|
contacts []string // Contacts list
|
||||||
|
currentContact string // Contact we are currently messaging
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Which window is on top currently on top of the other.
|
||||||
|
// This is the init setup
|
||||||
|
viewState = viewsState{
|
||||||
|
input: chatInputWindow,
|
||||||
|
side: menuWindow,
|
||||||
|
}
|
||||||
|
menuOptions = []string{contactList, rawMode, askServerForRoster, disconnect}
|
||||||
|
// Errors
|
||||||
|
servConnFail = errors.New("failed to connect to server. Check your configuration ? Exiting")
|
||||||
|
)
|
||||||
|
|
||||||
|
func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
|
||||||
|
if _, err := g.SetCurrentView(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return g.SetViewOnTop(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func layout(g *gocui.Gui) error {
|
||||||
|
maxX, maxY := g.Size()
|
||||||
|
|
||||||
|
if v, err := g.SetView(chatLogWindow, maxX/5, 0, maxX-1, 5*maxY/6-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = chatLogWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
v.Autoscroll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(contactsListWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = contactsListWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
// If we set this to true, the contacts list will "fit" in the window but if the number
|
||||||
|
// of contacts exceeds the maximum height, some contacts will be hidden...
|
||||||
|
// If set to false, we can scroll up and down the contact list... infinitely. Meaning lower lines
|
||||||
|
// will be unlimited and empty... Didn't find a way to quickfix yet.
|
||||||
|
v.Autoscroll = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = menuWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
v.Autoscroll = true
|
||||||
|
fmt.Fprint(v, strings.Join(menuOptions, "\n"))
|
||||||
|
if _, err = setCurrentViewOnTop(g, menuWindow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(rawInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = rawInputWindowTitle
|
||||||
|
v.Editable = true
|
||||||
|
v.Wrap = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(chatInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = chatInputWindowTitle
|
||||||
|
v.Editable = true
|
||||||
|
v.Wrap = true
|
||||||
|
|
||||||
|
if _, err = setCurrentViewOnTop(g, chatInputWindow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gocui.ErrQuit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an input text from the user to the backend while also printing it in the chatlog window.
|
||||||
|
// KeyEnter is viewed as "\n" by gocui, so messages should only be one line, whereas raw sending has a different key
|
||||||
|
// binding and therefor should work with this too (for multiple lines stanzas)
|
||||||
|
func writeInput(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
chatLogWindow, _ := g.View(chatLogWindow)
|
||||||
|
|
||||||
|
input := strings.Join(v.ViewBufferLines(), "\n")
|
||||||
|
|
||||||
|
fmt.Fprintln(chatLogWindow, "Me : ", input)
|
||||||
|
if viewState.input == rawInputWindow {
|
||||||
|
rawTextChan <- input
|
||||||
|
} else {
|
||||||
|
textChan <- input
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setKeyBindings(g *gocui.Gui) {
|
||||||
|
// ==========================
|
||||||
|
// All views
|
||||||
|
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Chat input
|
||||||
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Raw input
|
||||||
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlE, gocui.ModNone, writeInput); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Menu
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Contacts list
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Disconnect message
|
||||||
|
if err := g.SetKeybinding(disconnectMsg, gocui.KeyEnter, gocui.ModNone, delMsg); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// General
|
||||||
|
// Used to handle menu selections and navigations
|
||||||
|
func getLine(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
var l string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, cy := v.Cursor()
|
||||||
|
if l, err = v.Line(cy); err != nil {
|
||||||
|
l = ""
|
||||||
|
}
|
||||||
|
if viewState.side == menuWindow {
|
||||||
|
if l == contactList {
|
||||||
|
cv, _ := g.View(contactsListWindow)
|
||||||
|
viewState.side = contactsListWindow
|
||||||
|
g.SetViewOnTop(contactsListWindow)
|
||||||
|
g.SetCurrentView(contactsListWindow)
|
||||||
|
if len(cv.ViewBufferLines()) == 0 {
|
||||||
|
printContactsToWindow(g, viewState.contacts)
|
||||||
|
}
|
||||||
|
} else if l == disconnect {
|
||||||
|
maxX, maxY := g.Size()
|
||||||
|
msg := "You disconnected from the server. Press enter to quit."
|
||||||
|
if v, err := g.SetView(disconnectMsg, maxX/2-30, maxY/2, maxX/2-29+len(msg), maxY/2+2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(v, msg)
|
||||||
|
if _, err := g.SetCurrentView(disconnectMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
killChan <- disconnectErr
|
||||||
|
} else if l == askServerForRoster {
|
||||||
|
chlw, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Asking server for contacts list...")
|
||||||
|
rosterChan <- struct{}{}
|
||||||
|
} else if l == rawMode {
|
||||||
|
mw, _ := g.View(menuWindow)
|
||||||
|
viewState.input = rawInputWindow
|
||||||
|
g.SetViewOnTop(rawInputWindow)
|
||||||
|
g.SetCurrentView(rawInputWindow)
|
||||||
|
menuOptions[1] = messageMode
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
||||||
|
message := "Now sending in raw stanza mode"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
} else if l == messageMode {
|
||||||
|
mw, _ := g.View(menuWindow)
|
||||||
|
viewState.input = chatInputWindow
|
||||||
|
g.SetViewOnTop(chatInputWindow)
|
||||||
|
g.SetCurrentView(chatInputWindow)
|
||||||
|
menuOptions[1] = rawMode
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
||||||
|
message := "Now sending in messages mode"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
}
|
||||||
|
} else if viewState.side == contactsListWindow {
|
||||||
|
if l == backFromContacts {
|
||||||
|
viewState.side = menuWindow
|
||||||
|
g.SetViewOnTop(menuWindow)
|
||||||
|
g.SetCurrentView(menuWindow)
|
||||||
|
} else if l == "" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// Updating the current correspondent, back-end side.
|
||||||
|
CorrespChan <- l
|
||||||
|
viewState.currentContact = l
|
||||||
|
// Showing the selected contact in contacts list
|
||||||
|
cl, _ := g.View(contactsListWindow)
|
||||||
|
cts := cl.ViewBufferLines()
|
||||||
|
cl.Clear()
|
||||||
|
printContactsToWindow(g, cts)
|
||||||
|
// Showing a message to the user, and switching back to input after the new contact is selected.
|
||||||
|
message := "Now sending messages to : " + l + " in a private conversation"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
g.SetCurrentView(chatInputWindow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printContactsToWindow(g *gocui.Gui, contactsList []string) {
|
||||||
|
cl, _ := g.View(contactsListWindow)
|
||||||
|
for _, c := range contactsList {
|
||||||
|
c = strings.ReplaceAll(c, " *", "")
|
||||||
|
if c == viewState.currentContact {
|
||||||
|
fmt.Fprintf(cl, c+" *\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(cl, c+"\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing view between input and "menu/contacts" when pressing the specific key.
|
||||||
|
func nextView(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v == nil || v.Name() == chatInputWindow || v.Name() == rawInputWindow {
|
||||||
|
_, err := g.SetCurrentView(viewState.side)
|
||||||
|
return err
|
||||||
|
} else if v.Name() == menuWindow || v.Name() == contactsListWindow {
|
||||||
|
_, err := g.SetCurrentView(viewState.input)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not be reached right now
|
||||||
|
_, err := g.SetCurrentView(chatInputWindow)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v != nil {
|
||||||
|
cx, cy := v.Cursor()
|
||||||
|
// Avoid going below the list of contacts. Although lines are stored in the view as a slice
|
||||||
|
// in the used lib. Therefor, if the number of lines is too big, the cursor will go past the last line since
|
||||||
|
// increasing slice capacity is done by doubling it. Last lines will be "nil" and reachable by the cursor
|
||||||
|
// in a dynamic context (such as contacts list)
|
||||||
|
cv := g.CurrentView()
|
||||||
|
h := cv.LinesHeight()
|
||||||
|
if cy+1 >= h {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Lower cursor
|
||||||
|
if err := v.SetCursor(cx, cy+1); err != nil {
|
||||||
|
ox, oy := v.Origin()
|
||||||
|
if err := v.SetOrigin(ox, oy+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v != nil {
|
||||||
|
ox, oy := v.Origin()
|
||||||
|
cx, cy := v.Cursor()
|
||||||
|
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
|
||||||
|
if err := v.SetOrigin(ox, oy-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func delMsg(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := g.DeleteView(disconnectMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
errChan <- gocui.ErrQuit // Quit the program
|
||||||
|
return nil
|
||||||
|
}
|
339
_examples/xmpp_chat_client/xmpp_chat_client.go
Normal file
339
_examples/xmpp_chat_client/xmpp_chat_client.go
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/awesome-gocui/gocui"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
infoFormat = "====== "
|
||||||
|
// Default configuration
|
||||||
|
defaultConfigFilePath = "./"
|
||||||
|
|
||||||
|
configFileName = "config"
|
||||||
|
configType = "yaml"
|
||||||
|
logStanzasOn = "logger_on"
|
||||||
|
logFilePath = "logfile_path"
|
||||||
|
// Keys in config
|
||||||
|
serverAddressKey = "full_address"
|
||||||
|
clientJid = "jid"
|
||||||
|
clientPass = "pass"
|
||||||
|
configContactSep = ";"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CorrespChan = make(chan string, 1)
|
||||||
|
textChan = make(chan string, 5)
|
||||||
|
rawTextChan = make(chan string, 5)
|
||||||
|
killChan = make(chan error, 1)
|
||||||
|
errChan = make(chan error)
|
||||||
|
rosterChan = make(chan struct{})
|
||||||
|
|
||||||
|
logger *log.Logger
|
||||||
|
disconnectErr = errors.New("disconnecting client")
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Server map[string]string `mapstructure:"server"`
|
||||||
|
Client map[string]string `mapstructure:"client"`
|
||||||
|
Contacts string `string:"contact"`
|
||||||
|
LogStanzas map[string]string `mapstructure:"logstanzas"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Parse the flag with the config directory path as argument
|
||||||
|
flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+
|
||||||
|
" file you want to use. Config file should be named \"config\" and be in YAML format..")
|
||||||
|
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Read configuration
|
||||||
|
c := readConfig()
|
||||||
|
|
||||||
|
//================================
|
||||||
|
// Setup logger
|
||||||
|
on, err := strconv.ParseBool(c.LogStanzas[logStanzasOn])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if on {
|
||||||
|
f, err := os.OpenFile(path.Join(c.LogStanzas[logFilePath], "logs.txt"), os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
logger = log.New(f, "", log.Lshortfile|log.Ldate|log.Ltime)
|
||||||
|
logger.SetOutput(f)
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Create TUI
|
||||||
|
g, err := gocui.NewGui(gocui.OutputNormal, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
defer g.Close()
|
||||||
|
g.Highlight = true
|
||||||
|
g.Cursor = true
|
||||||
|
g.SelFgColor = gocui.ColorGreen
|
||||||
|
g.SetManagerFunc(layout)
|
||||||
|
setKeyBindings(g)
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Run TUI
|
||||||
|
go func() {
|
||||||
|
errChan <- g.MainLoop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Start XMPP client
|
||||||
|
go startClient(g, c)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err == gocui.ErrQuit {
|
||||||
|
log.Println("Closing client.")
|
||||||
|
} else {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startClient(g *gocui.Gui, config *config) {
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client setup
|
||||||
|
clientCfg := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: config.Server[serverAddressKey],
|
||||||
|
},
|
||||||
|
Jid: config.Client[clientJid],
|
||||||
|
Credential: xmpp.Password(config.Client[clientPass]),
|
||||||
|
Insecure: true}
|
||||||
|
|
||||||
|
var client *xmpp.Client
|
||||||
|
var err error
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
|
||||||
|
handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(msg)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := g.View(chatLogWindow)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(v, "%sIgnoring packet: %T\n", infoFormat, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
if msg.Error.Code != 0 {
|
||||||
|
_, err := fmt.Fprintf(v, "Error from server : %s : %s \n", msg.Error.Reason, msg.XMLName.Space)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(strings.TrimSpace(msg.Body)) != 0 {
|
||||||
|
_, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HandleFunc("message", handlerWithGui)
|
||||||
|
if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil {
|
||||||
|
log.Panicln(fmt.Sprintf("Could not create a new client ! %s", err))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client connection
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
msg := fmt.Sprintf("%sXMPP connection failed: %s", infoFormat, err)
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
v, err := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintf(v, msg)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
fmt.Println("Failed to connect to server. Exiting...")
|
||||||
|
errChan <- servConnFail
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Start working
|
||||||
|
updateRosterFromConfig(config)
|
||||||
|
// Sending the default contact in a channel. Default value is the first contact in the list from the config.
|
||||||
|
viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0]
|
||||||
|
// Informing user of the default contact
|
||||||
|
clw, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintf(clw, infoFormat+"Now sending messages to "+viewState.currentContact+" in a private conversation\n")
|
||||||
|
CorrespChan <- viewState.currentContact
|
||||||
|
startMessaging(client, config, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMessaging(client xmpp.Sender, config *config, g *gocui.Gui) {
|
||||||
|
var text string
|
||||||
|
var correspondent string
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-killChan:
|
||||||
|
if err == disconnectErr {
|
||||||
|
sc := client.(xmpp.StreamClient)
|
||||||
|
sc.Disconnect()
|
||||||
|
} else {
|
||||||
|
logger.Println(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case text = <-textChan:
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, Type: stanza.MessageTypeChat}, Body: text}
|
||||||
|
if logger != nil {
|
||||||
|
raw, _ := xml.Marshal(reply)
|
||||||
|
logger.Println(string(raw))
|
||||||
|
}
|
||||||
|
err := client.Send(reply)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("There was a problem sending the message : %v", reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case text = <-rawTextChan:
|
||||||
|
if logger != nil {
|
||||||
|
logger.Println(text)
|
||||||
|
}
|
||||||
|
err := client.SendRaw(text)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("There was a problem sending the message : %v", text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case crrsp := <-CorrespChan:
|
||||||
|
correspondent = crrsp
|
||||||
|
case <-rosterChan:
|
||||||
|
askForRoster(client, g, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only reads and parses the configuration
|
||||||
|
func readConfig() *config {
|
||||||
|
viper.SetConfigName(configFileName) // name of config file (without extension)
|
||||||
|
viper.BindPFlags(pflag.CommandLine)
|
||||||
|
viper.AddConfigPath(viper.GetString("c")) // path to look for the config file in
|
||||||
|
err := viper.ReadInConfig() // Find and read the config file
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
log.Fatalf("%s %s", err, "Please make sure you give a path to the directory of the config and not to the config itself.")
|
||||||
|
} else {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigType(configType)
|
||||||
|
var config config
|
||||||
|
err = viper.Unmarshal(&config)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("Unable to decode Config: %s \n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have contacts to message
|
||||||
|
if len(strings.TrimSpace(config.Contacts)) == 0 {
|
||||||
|
log.Panicln("You appear to have no contacts to message !")
|
||||||
|
}
|
||||||
|
// Check logging
|
||||||
|
config.LogStanzas[logFilePath] = path.Clean(config.LogStanzas[logFilePath])
|
||||||
|
on, err := strconv.ParseBool(config.LogStanzas[logStanzasOn])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if d, e := isDirectory(config.LogStanzas[logFilePath]); (e != nil || !d) && on {
|
||||||
|
log.Panicln("The log file path could not be found or is not a directory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an error occurs, this is used to kill the client
|
||||||
|
func errorHandler(err error) {
|
||||||
|
killChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the client roster from the config. This does not check with the server that the roster is correct.
|
||||||
|
// If user tries to send a message to someone not registered with the server, the server will return an error.
|
||||||
|
func updateRosterFromConfig(config *config) {
|
||||||
|
viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts)
|
||||||
|
// Put a "go back" button at the end of the list
|
||||||
|
viewState.contacts = append(viewState.contacts, backFromContacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the menu panel of the view with the current user's roster, by asking the server.
|
||||||
|
func askForRoster(client xmpp.Sender, g *gocui.Gui, config *config) {
|
||||||
|
// Craft a roster request
|
||||||
|
req := stanza.NewIQ(stanza.Attrs{From: config.Client[clientJid], Type: stanza.IQTypeGet})
|
||||||
|
req.RosterItems()
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(req)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
|
||||||
|
// Send the roster request to the server
|
||||||
|
c, err := client.SendIQ(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending a IQ has a channel spawned to process the response once we receive it.
|
||||||
|
// In order not to block the client, we spawn a goroutine to update the TUI once the server has responded.
|
||||||
|
go func() {
|
||||||
|
serverResp := <-c
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(serverResp)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
// Update contacts with the response from the server
|
||||||
|
chlw, _ := g.View(chatLogWindow)
|
||||||
|
if rosterItems, ok := serverResp.Payload.(*stanza.RosterItems); ok {
|
||||||
|
viewState.contacts = []string{}
|
||||||
|
for _, item := range rosterItems.Items {
|
||||||
|
viewState.contacts = append(viewState.contacts, item.Jid)
|
||||||
|
}
|
||||||
|
// Put a "go back" button at the end of the list
|
||||||
|
viewState.contacts = append(viewState.contacts, backFromContacts)
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Contacts list updated !")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Failed to update contact list !")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDirectory(path string) (bool, error) {
|
||||||
|
fileInfo, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return fileInfo.IsDir(), err
|
||||||
|
}
|
22
_examples/xmpp_component/README.md
Normal file
22
_examples/xmpp_component/README.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# xmpp_component
|
||||||
|
|
||||||
|
This component will connect to ejabberd and act as a subdomain "service" of your primary XMPP domain
|
||||||
|
(in that case localhost).
|
||||||
|
|
||||||
|
This component does nothing expect connect and show up in service discovery.
|
||||||
|
|
||||||
|
To be able to connect this component, you need to add a listener to your XMPP server.
|
||||||
|
|
||||||
|
Here is an example ejabberd configuration for that component listener:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
listen:
|
||||||
|
...
|
||||||
|
-
|
||||||
|
port: 8888
|
||||||
|
module: ejabberd_service
|
||||||
|
password: "mypass"
|
||||||
|
```
|
||||||
|
|
||||||
|
ejabberd will listen for a component (service) on port 8888 and allows it to connect using the
|
||||||
|
secret "mypass".
|
119
_examples/xmpp_component/xmpp_component.go
Normal file
119
_examples/xmpp_component/xmpp_component.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:8888",
|
||||||
|
Domain: "service2.localhost",
|
||||||
|
},
|
||||||
|
Domain: "service2.localhost",
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: "Test Component",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces(stanza.NSDiscoInfo).
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
discoInfo(s, p, opts)
|
||||||
|
})
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces(stanza.NSDiscoItems).
|
||||||
|
HandlerFunc(discoItems)
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces("jabber:iq:version").
|
||||||
|
HandlerFunc(handleVersion)
|
||||||
|
|
||||||
|
component, err := xmpp.NewComponent(opts, router, handleError)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the component to a stream manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
|
||||||
|
cm := xmpp.NewStreamManager(component, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Received message:", msg.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
|
// TODO: fix this...
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
disco := iqResp.DiscoInfo()
|
||||||
|
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||||
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle iq error responses
|
||||||
|
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
discoItems, ok := iq.Payload.(*stanza.DiscoItems)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix this...
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := iqResp.DiscoItems()
|
||||||
|
|
||||||
|
if discoItems.Node == "" {
|
||||||
|
items.AddItem("service.localhost", "node1", "test node")
|
||||||
|
}
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleVersion(c xmpp.Sender, p stanza.Packet) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
4
_examples/xmpp_component2/README.md
Normal file
4
_examples/xmpp_component2/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# xmpp_component2
|
||||||
|
|
||||||
|
|
||||||
|
This program is an example of the simplest XMPP component: it connects to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response.
|
79
_examples/xmpp_component2/main.go
Normal file
79
_examples/xmpp_component2/main.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Connect to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
domain = "mycomponent.localhost"
|
||||||
|
address = "build.vpn.p1:8888"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init and return a component
|
||||||
|
func makeComponent() *xmpp.Component {
|
||||||
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: address,
|
||||||
|
Domain: domain,
|
||||||
|
},
|
||||||
|
Domain: domain,
|
||||||
|
Secret: "secret",
|
||||||
|
}
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
c, err := xmpp.NewComponent(opts, router, handleError)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := makeComponent()
|
||||||
|
|
||||||
|
// Connect Component to the server
|
||||||
|
fmt.Printf("Connecting to %v\n", address)
|
||||||
|
err := c.Connect()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a disco iq
|
||||||
|
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet,
|
||||||
|
From: domain,
|
||||||
|
To: "localhost",
|
||||||
|
Id: "my-iq1"})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
|
||||||
|
// res is the channel used to receive the result iq
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
res, _ := c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case iqResponse := <-res:
|
||||||
|
// Got response from server
|
||||||
|
fmt.Print(iqResponse.Payload)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
panic("No iq response was received in time")
|
||||||
|
}
|
||||||
|
}
|
56
_examples/xmpp_echo/xmpp_echo.go
Normal file
56
_examples/xmpp_echo/xmpp_echo.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
xmpp_echo is a demo client that connect on an XMPP server and echo message received back to original sender.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("test"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
|
_ = s.Send(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
37
_examples/xmpp_jukebox/README.md
Normal file
37
_examples/xmpp_jukebox/README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Jukebox example
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- You need mpg123 installed on your computer because the example runs it as a command :
|
||||||
|
[Official MPG123 website](https://mpg123.de/)
|
||||||
|
Most linux distributions have a package for it.
|
||||||
|
- You need a soundcloud ID to play a music from the website through mpg123. You currently cannot play music files with this example.
|
||||||
|
Your user ID is available in your account settings on the [soundcloud website](https://soundcloud.com/)
|
||||||
|
**One is provided for convenience.**
|
||||||
|
- You need a running jabber server. You can run your local instance of [ejabberd](https://www.ejabberd.im/) for example.
|
||||||
|
- You need a registered user on the running jabber server.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
You can edit the soundcloud ID in the example file with your own, or use the provided one :
|
||||||
|
```go
|
||||||
|
const scClientID = "dde6a0075614ac4f3bea423863076b22"
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the example, build it with (while in the example directory) :
|
||||||
|
```
|
||||||
|
go build xmpp_jukebox.go
|
||||||
|
```
|
||||||
|
|
||||||
|
then run it with (update the command arguments accordingly):
|
||||||
|
```
|
||||||
|
./xmpp_jukebox -jid=MY_USERE@MY_DOMAIN/jukebox -password=MY_PASSWORD -address=MY_SERVER:MY_SERVER_PORT
|
||||||
|
```
|
||||||
|
Make sure to have a resource, for instance "/jukebox", on your jid.
|
||||||
|
|
||||||
|
Then you can send the following stanza to "MY_USERE@MY_DOMAIN/jukebox" (with the resource) to play a song (update the soundcloud URL accordingly) :
|
||||||
|
```xml
|
||||||
|
<iq id="1" to="MY_USERE@MY_DOMAIN/jukebox" type="set">
|
||||||
|
<set xml:lang="en" xmlns="urn:xmpp:iot:control">
|
||||||
|
<string name="url" value="https://soundcloud.com/UPDATE/ME"/>
|
||||||
|
</set>
|
||||||
|
</iq>
|
||||||
|
```
|
149
_examples/xmpp_jukebox/xmpp_jukebox.go
Normal file
149
_examples/xmpp_jukebox/xmpp_jukebox.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// Can be launched with:
|
||||||
|
// ./xmpp_jukebox -jid=test@localhost/jukebox -password=test -address=localhost:5222
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/processone/mpg123"
|
||||||
|
"github.com/processone/soundcloud"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the actual song Stream URL from SoundCloud website song URL and play it with mpg123 player.
|
||||||
|
const scClientID = "dde6a0075614ac4f3bea423863076b22"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
jid := flag.String("jid", "", "jukebok XMPP Jid, resource is optional")
|
||||||
|
password := flag.String("password", "", "XMPP account password")
|
||||||
|
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 1. Create mpg player
|
||||||
|
player, err := mpg123.NewPlayer()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Prepare XMPP client
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: *address,
|
||||||
|
},
|
||||||
|
Jid: *jid,
|
||||||
|
Credential: xmpp.Password(*password),
|
||||||
|
// StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.NewRoute().
|
||||||
|
Packet("message").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
handleMessage(s, p, player)
|
||||||
|
})
|
||||||
|
router.NewRoute().
|
||||||
|
Packet("iq").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
handleIQ(s, p, player)
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command := strings.Trim(msg.Body, " ")
|
||||||
|
if command == "stop" {
|
||||||
|
player.Stop()
|
||||||
|
} else {
|
||||||
|
playSCURL(player, command)
|
||||||
|
sendUserTune(s, "Radiohead", "Spectre")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch payload := iq.Payload.(type) {
|
||||||
|
// We support IOT Control IQ
|
||||||
|
case *stanza.ControlSet:
|
||||||
|
var url string
|
||||||
|
for _, element := range payload.Fields {
|
||||||
|
if element.XMLName.Local == "string" && element.Name == "url" {
|
||||||
|
url = strings.Trim(element.Value, " ")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playSCURL(player, url)
|
||||||
|
setResponse := new(stanza.ControlSetResponse)
|
||||||
|
// FIXME: Broken
|
||||||
|
reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
|
||||||
|
_ = s.Send(&reply)
|
||||||
|
// TODO add Soundclound artist / title retrieval
|
||||||
|
sendUserTune(s, "Radiohead", "Spectre")
|
||||||
|
default:
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", iq.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
||||||
|
rq, err := stanza.NewPublishItemRq("localhost",
|
||||||
|
"http://jabber.org/protocol/tune",
|
||||||
|
"",
|
||||||
|
stanza.Item{
|
||||||
|
XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"},
|
||||||
|
Any: &stanza.Node{
|
||||||
|
Nodes: []stanza.Node{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Local: "artist"},
|
||||||
|
Content: artist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Local: "title"},
|
||||||
|
Content: title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to build the publish request : %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.Send(rq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func playSCURL(p *mpg123.Player, rawURL string) {
|
||||||
|
songID, _ := soundcloud.GetSongID(rawURL)
|
||||||
|
// TODO: Maybe we need to check the track itself to get the stream URL from reply ?
|
||||||
|
url := soundcloud.FormatStreamURL(songID)
|
||||||
|
|
||||||
|
_ = p.Play(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// - Have a player API to play, play next, or add to queue
|
||||||
|
// - Have the ability to parse custom packet to play sound
|
||||||
|
// - Use PEP to display tunes status
|
||||||
|
// - Ability to "speak" messages
|
54
_examples/xmpp_oauth2/xmpp_oauth2.go
Normal file
54
_examples/xmpp_oauth2/xmpp_oauth2.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
xmpp_oauth2 is a demo client that connect on an XMPP server using OAuth2 and prints received messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.OAuthToken("OdAIsBlY83SLBaqQoClAn7vrZSHxixT8"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
// Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
}
|
17
_examples/xmpp_pubsub_client/README.md
Normal file
17
_examples/xmpp_pubsub_client/README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# PubSub client example
|
||||||
|
|
||||||
|
## Description
|
||||||
|
This is a simple example of a client that :
|
||||||
|
* Creates a node on a service
|
||||||
|
* Subscribes to that node
|
||||||
|
* Publishes to that node
|
||||||
|
* Gets the notification from the publication and prints it on screen
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
You need to have a running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html).
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
Just run :
|
||||||
|
```
|
||||||
|
go run xmpp_ps_client.go
|
||||||
|
```
|
278
_examples/xmpp_pubsub_client/xmpp_ps_client.go
Normal file
278
_examples/xmpp_pubsub_client/xmpp_ps_client.go
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userJID = "testuser2@localhost"
|
||||||
|
serverAddress = "localhost:5222"
|
||||||
|
nodeName = "lel_node"
|
||||||
|
serviceName = "pubsub.localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
var invalidResp = errors.New("invalid response")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: serverAddress,
|
||||||
|
},
|
||||||
|
Jid: userJID,
|
||||||
|
Credential: xmpp.Password("pass123"),
|
||||||
|
// StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.NewRoute().Packet("message").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
data, _ := xml.Marshal(p)
|
||||||
|
log.Println("Received a message ! => \n" + string(data))
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) })
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client connection
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Create a node
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
createNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ================================================================================
|
||||||
|
// Configure the node. This can also be done in a single message with the creation
|
||||||
|
configureNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// Subscribe to this node :
|
||||||
|
subToNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Publish to that node
|
||||||
|
pubToNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Let's purge the node :
|
||||||
|
purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName)
|
||||||
|
purgeCh, err := client.SendIQ(ctx, purgeRq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not send purge request: %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case purgeResp := <-purgeCh:
|
||||||
|
|
||||||
|
if purgeResp.Type == stanza.IQTypeError {
|
||||||
|
cancel()
|
||||||
|
if vld, err := purgeResp.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", purgeResp, err)
|
||||||
|
}
|
||||||
|
log.Fatalf("error while purging node : %s", purgeResp.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println("node successfully purged")
|
||||||
|
case <-time.After(1000 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while purging node")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
rqCreate, err := stanza.NewCreateNode(serviceName, nodeName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
createCh, err := client.SendIQ(ctx, rqCreate)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if createCh != nil {
|
||||||
|
select {
|
||||||
|
case respCr := <-createCh:
|
||||||
|
// Got response from server
|
||||||
|
if respCr.Type == stanza.IQTypeError {
|
||||||
|
if vld, err := respCr.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %+v"+" reason: %s", respCr, err)
|
||||||
|
}
|
||||||
|
if respCr.Error.Reason != "conflict" {
|
||||||
|
log.Fatalf("%+v", respCr.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println(respCr.Error.Text)
|
||||||
|
} else {
|
||||||
|
fmt.Print("successfully created channel")
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while creating node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
// First, ask for a form with the config options
|
||||||
|
confRq, _ := stanza.NewConfigureNode(serviceName, nodeName)
|
||||||
|
confReqCh, err := client.SendIQ(ctx, confRq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not send iq : %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case confForm := <-confReqCh:
|
||||||
|
// If the request was successful, we now have a form with configuration options to update
|
||||||
|
fields, err := confForm.GetFormFields()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("No config fields found !")
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are some common fields expected to be present. Change processing to your liking
|
||||||
|
if fields["pubsub#max_payload_size"] != nil {
|
||||||
|
fields["pubsub#max_payload_size"].ValuesList[0] = "100000"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fields["pubsub#notification_type"] != nil {
|
||||||
|
fields["pubsub#notification_type"].ValuesList[0] = "headline"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the modified fields as a form
|
||||||
|
submitConf, err := stanza.NewFormSubmissionOwner(serviceName,
|
||||||
|
nodeName,
|
||||||
|
[]*stanza.Field{
|
||||||
|
fields["pubsub#max_payload_size"],
|
||||||
|
fields["pubsub#notification_type"],
|
||||||
|
})
|
||||||
|
|
||||||
|
c, _ := client.SendIQ(ctx, submitConf)
|
||||||
|
select {
|
||||||
|
case confResp := <-c:
|
||||||
|
if confResp.Type == stanza.IQTypeError {
|
||||||
|
cancel()
|
||||||
|
if vld, err := confResp.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", confResp, err)
|
||||||
|
}
|
||||||
|
log.Fatalf("node configuration failed : %s", confResp.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println("node configuration was successful")
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while configuring the node")
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while asking for the config form")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func subToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
rqSubscribe, err := stanza.NewSubRq(serviceName, stanza.SubInfo{
|
||||||
|
Node: nodeName,
|
||||||
|
Jid: userJID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
subRespCh, _ := client.SendIQ(ctx, rqSubscribe)
|
||||||
|
if subRespCh != nil {
|
||||||
|
select {
|
||||||
|
case <-subRespCh:
|
||||||
|
log.Println("Subscribed to the service")
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while subscribing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
pub, err := stanza.NewPublishItemRq(serviceName, nodeName, "", stanza.Item{
|
||||||
|
Publisher: "testuser2",
|
||||||
|
Any: &stanza.Node{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://www.w3.org/2005/Atom",
|
||||||
|
Local: "entry",
|
||||||
|
},
|
||||||
|
Nodes: []stanza.Node{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "title"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item title",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "summary"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item content summary",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "link"},
|
||||||
|
Attrs: []xml.Attr{
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "rel"},
|
||||||
|
Value: "alternate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "type"},
|
||||||
|
Value: "text/html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "href"},
|
||||||
|
Value: "http://denmark.lit/2003/12/13/atom03",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "id"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item content ID",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "published"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "2003-12-13T18:30:02Z",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "updated"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "2003-12-13T18:30:02Z",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
pubRespCh, _ := client.SendIQ(ctx, pub)
|
||||||
|
if pubRespCh != nil {
|
||||||
|
select {
|
||||||
|
case <-pubRespCh:
|
||||||
|
log.Println("Published item to the service")
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while publishing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
_examples/xmpp_websocket/xmpp_websocket.go
Normal file
52
_examples/xmpp_websocket/xmpp_websocket.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
xmpp_websocket is a demo client that connect on an XMPP server using websocket and prints received messages.ß
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "wss://localhost:5443/ws",
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("test"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
}
|
105
auth.go
Normal file
105
auth.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credential is used to pass the type of secret that will be used to connect to XMPP server.
|
||||||
|
// It can be either a password or an OAuth 2 bearer token.
|
||||||
|
type Credential struct {
|
||||||
|
secret string
|
||||||
|
mechanisms []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Password(pwd string) Credential {
|
||||||
|
credential := Credential{
|
||||||
|
secret: pwd,
|
||||||
|
mechanisms: []string{"PLAIN"},
|
||||||
|
}
|
||||||
|
return credential
|
||||||
|
}
|
||||||
|
|
||||||
|
func OAuthToken(token string) Credential {
|
||||||
|
credential := Credential{
|
||||||
|
secret: token,
|
||||||
|
mechanisms: []string{"X-OAUTH2"},
|
||||||
|
}
|
||||||
|
return credential
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Authentication flow for SASL mechanisms
|
||||||
|
|
||||||
|
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, credential Credential) (err error) {
|
||||||
|
var matchingMech string
|
||||||
|
for _, mech := range credential.mechanisms {
|
||||||
|
if isSupportedMech(mech, f.Mechanisms.Mechanism) {
|
||||||
|
matchingMech = mech
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch matchingMech {
|
||||||
|
case "PLAIN", "X-OAUTH2":
|
||||||
|
// TODO: Implement other type of SASL mechanisms
|
||||||
|
return authPlain(socket, decoder, matchingMech, user, credential.secret)
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("no matching authentication (%v) supported by server: %v", credential.mechanisms, f.Mechanisms.Mechanism)
|
||||||
|
return NewConnError(err, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain authentication: send base64-encoded \x00 user \x00 password
|
||||||
|
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user string, secret string) error {
|
||||||
|
raw := "\x00" + user + "\x00" + secret
|
||||||
|
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||||
|
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||||
|
|
||||||
|
a := stanza.SASLAuth{
|
||||||
|
Mechanism: mech,
|
||||||
|
Value: string(enc),
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n, err := socket.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if n == 0 {
|
||||||
|
return errors.New("failed to write authSASL nonza to socket : wrote 0 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next message should be either success or failure.
|
||||||
|
val, err := stanza.NextPacket(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case stanza.SASLSuccess:
|
||||||
|
case stanza.SASLFailure:
|
||||||
|
// v.Any is type of sub-element in failure, which gives a description of what failed.
|
||||||
|
err := errors.New("auth failure: " + v.Any.Local)
|
||||||
|
return NewConnError(err, true)
|
||||||
|
default:
|
||||||
|
return errors.New("expected SASL success or failure, got " + v.Name())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSupportedMech returns true if the mechanism is supported in the provided list.
|
||||||
|
func isSupportedMech(mech string, mechanisms []string) bool {
|
||||||
|
for _, m := range mechanisms {
|
||||||
|
if mech == m {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
101
backoff.go
Normal file
101
backoff.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
Interesting reference on backoff:
|
||||||
|
- Exponential Backoff And Jitter (AWS Blog):
|
||||||
|
https://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||||
|
|
||||||
|
We use Jitter as a default for exponential backoff, as the goal of
|
||||||
|
this module is not to provide precise 'ticks', but good behaviour to
|
||||||
|
implement retries that are helping the server to recover faster in
|
||||||
|
case of congestion.
|
||||||
|
|
||||||
|
It can be used in several ways:
|
||||||
|
- Using duration to get next sleep time.
|
||||||
|
- Using ticker channel to trigger callback function on tick
|
||||||
|
|
||||||
|
The functions for Backoff are not threadsafe, but you can:
|
||||||
|
- Keep the attempt counter on your end and use durationForAttempt(int)
|
||||||
|
- Use lock in your own code to protect the Backoff structure.
|
||||||
|
|
||||||
|
TODO: Implement Backoff Ticker channel
|
||||||
|
TODO: Implement throttler interface. Throttler could be used to implement various reconnect strategies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultBase int = 20 // Backoff base, in ms
|
||||||
|
defaultFactor int = 2
|
||||||
|
defaultCap int = 180000 // 3 minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
// backoff provides increasing duration with the number of attempt
|
||||||
|
// performed. The structure is used to support exponential backoff on
|
||||||
|
// connection attempts to avoid hammering the server we are connecting
|
||||||
|
// to.
|
||||||
|
type backoff struct {
|
||||||
|
NoJitter bool
|
||||||
|
Base int
|
||||||
|
Factor int
|
||||||
|
Cap int
|
||||||
|
lastDuration int
|
||||||
|
attempt int
|
||||||
|
}
|
||||||
|
|
||||||
|
// duration returns the duration to apply to the current attempt.
|
||||||
|
func (b *backoff) duration() time.Duration {
|
||||||
|
d := b.durationForAttempt(b.attempt)
|
||||||
|
b.attempt++
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait sleeps for backoff duration for current attempt.
|
||||||
|
func (b *backoff) wait() {
|
||||||
|
time.Sleep(b.duration())
|
||||||
|
}
|
||||||
|
|
||||||
|
// durationForAttempt returns a duration for an attempt number, in a stateless way.
|
||||||
|
func (b *backoff) durationForAttempt(attempt int) time.Duration {
|
||||||
|
b.setDefault()
|
||||||
|
expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt)))
|
||||||
|
d := int(math.Trunc(expBackoff))
|
||||||
|
if !b.NoJitter {
|
||||||
|
d = rand.Intn(d)
|
||||||
|
}
|
||||||
|
return time.Duration(d) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset sets back the number of attempts to 0. This is to be called after a successful operation has been performed,
|
||||||
|
// to reset the exponential backoff interval.
|
||||||
|
func (b *backoff) reset() {
|
||||||
|
b.attempt = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backoff) setDefault() {
|
||||||
|
if b.Base == 0 {
|
||||||
|
b.Base = defaultBase
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Cap == 0 {
|
||||||
|
b.Cap = defaultCap
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Factor == 0 {
|
||||||
|
b.Factor = defaultFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We use full jitter as default for now as it seems to provide good behaviour for reconnect.
|
||||||
|
|
||||||
|
Base is the default interval between attempts (if backoff Factor was equal to 1)
|
||||||
|
|
||||||
|
Attempt is the number of retry for operation. If we start attempt at 0, first sleep equals base.
|
||||||
|
|
||||||
|
Cap is the maximum sleep time duration we tolerate between attempts
|
||||||
|
*/
|
22
backoff_test.go
Normal file
22
backoff_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDurationForAttempt_NoJitter(t *testing.T) {
|
||||||
|
b := backoff{Base: 25, NoJitter: true}
|
||||||
|
bInMS := time.Duration(b.Base) * time.Millisecond
|
||||||
|
if b.durationForAttempt(0) != bInMS {
|
||||||
|
t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.durationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond)
|
||||||
|
}
|
||||||
|
var prevDuration, d time.Duration
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
d = b.durationForAttempt(i)
|
||||||
|
if !(d >= prevDuration) {
|
||||||
|
t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration)
|
||||||
|
}
|
||||||
|
prevDuration = d
|
||||||
|
}
|
||||||
|
}
|
12
bi_dir_iterator.go
Normal file
12
bi_dir_iterator.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
type BiDirIterator interface {
|
||||||
|
// Next returns the next element of this iterator, if a response is available within t milliseconds
|
||||||
|
Next(t int) (BiDirIteratorElt, error)
|
||||||
|
// Previous returns the previous element of this iterator, if a response is available within t milliseconds
|
||||||
|
Previous(t int) (BiDirIteratorElt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BiDirIteratorElt interface {
|
||||||
|
NoOp()
|
||||||
|
}
|
151
cert_checker.go
Normal file
151
cert_checker.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Should I move this as an extension of the client?
|
||||||
|
// I should probably make the code more modular, but keep concern separated to keep it simple.
|
||||||
|
type ServerCheck struct {
|
||||||
|
address string
|
||||||
|
domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChecker(address, domain string) (*ServerCheck, error) {
|
||||||
|
client := ServerCheck{}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var host string
|
||||||
|
if client.address, host, err = extractParams(address); err != nil {
|
||||||
|
return &client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if domain != "" {
|
||||||
|
client.domain = domain
|
||||||
|
} else {
|
||||||
|
client.domain = host
|
||||||
|
}
|
||||||
|
|
||||||
|
return &client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check triggers actual TCP connection, based on previously defined parameters.
|
||||||
|
func (c *ServerCheck) Check() error {
|
||||||
|
var tcpconn net.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
timeout := 15 * time.Second
|
||||||
|
tcpconn, err = net.DialTimeout("tcp", c.address, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := xml.NewDecoder(tcpconn)
|
||||||
|
|
||||||
|
// Send stream open tag
|
||||||
|
if _, err = fmt.Fprintf(tcpconn, clientStreamOpen, c.domain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set xml decoder and extract streamID from reply (not used for now)
|
||||||
|
_, err = stanza.InitStream(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract stream features
|
||||||
|
var f stanza.StreamFeatures
|
||||||
|
packet, err := stanza.NextPacket(decoder)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("stream open decode features: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p := packet.(type) {
|
||||||
|
case stanza.StreamFeatures:
|
||||||
|
f = p
|
||||||
|
case stanza.StreamError:
|
||||||
|
return errors.New("open stream error: " + p.Error.Local)
|
||||||
|
default:
|
||||||
|
return errors.New("expected packet received while expecting features, got " + p.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := f.DoesStartTLS(); ok {
|
||||||
|
_, err = fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var k stanza.TLSProceed
|
||||||
|
if err = decoder.DecodeElement(&k, nil); err != nil {
|
||||||
|
return fmt.Errorf("expecting starttls proceed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tlsConfig tls.Config
|
||||||
|
tlsConfig.ServerName = c.domain
|
||||||
|
tlsConn := tls.Client(tcpconn, &tlsConfig)
|
||||||
|
// We convert existing connection to TLS
|
||||||
|
if err = tlsConn.Handshake(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We check that cert matches hostname
|
||||||
|
if err = tlsConn.VerifyHostname(c.domain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = checkExpiration(tlsConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("TLS not supported on server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expiration date for the whole certificate chain and returns an error
|
||||||
|
// if the expiration date is in less than 48 hours.
|
||||||
|
func checkExpiration(tlsConn *tls.Conn) error {
|
||||||
|
checkedCerts := make(map[string]struct{})
|
||||||
|
for _, chain := range tlsConn.ConnectionState().VerifiedChains {
|
||||||
|
for _, cert := range chain {
|
||||||
|
if _, checked := checkedCerts[string(cert.Signature)]; checked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
checkedCerts[string(cert.Signature)] = struct{}{}
|
||||||
|
|
||||||
|
// Check the expiration.
|
||||||
|
timeNow := time.Now()
|
||||||
|
expiresInHours := int64(cert.NotAfter.Sub(timeNow).Hours())
|
||||||
|
// fmt.Printf("Cert '%s' expires in %d days\n", cert.Subject.CommonName, expiresInHours/24)
|
||||||
|
if expiresInHours <= 48 {
|
||||||
|
return fmt.Errorf("certificate '%s' will expire on %s", cert.Subject.CommonName, cert.NotAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractParams(addr string) (string, string, error) {
|
||||||
|
var err error
|
||||||
|
hostport := strings.Split(addr, ":")
|
||||||
|
if len(hostport) > 2 {
|
||||||
|
err = errors.New("too many colons in xmpp server address")
|
||||||
|
return addr, hostport[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address is composed of two parts, we are good
|
||||||
|
if len(hostport) == 2 && hostport[1] != "" {
|
||||||
|
return addr, hostport[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port was not passed, we append XMPP default port:
|
||||||
|
return strings.Join([]string{hostport[0], "5222"}, ":"), hostport[0], err
|
||||||
|
}
|
444
client.go
Normal file
444
client.go
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// EventManager
|
||||||
|
|
||||||
|
// SyncConnState represents the current connection state.
|
||||||
|
type SyncConnState struct {
|
||||||
|
sync.RWMutex
|
||||||
|
// Current state of the client. Please use the dedicated getter and setter for this field as they are thread safe.
|
||||||
|
state ConnState
|
||||||
|
}
|
||||||
|
type ConnState = uint8
|
||||||
|
|
||||||
|
// getState is a thread-safe getter for the current state
|
||||||
|
func (scs *SyncConnState) getState() ConnState {
|
||||||
|
var res ConnState
|
||||||
|
scs.RLock()
|
||||||
|
res = scs.state
|
||||||
|
scs.RUnlock()
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// setState is a thread-safe setter for the current
|
||||||
|
func (scs *SyncConnState) setState(cs ConnState) {
|
||||||
|
scs.Lock()
|
||||||
|
scs.state = cs
|
||||||
|
scs.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a the list of events happening on the connection that the
|
||||||
|
// client can be notified about.
|
||||||
|
const (
|
||||||
|
StateDisconnected ConnState = iota
|
||||||
|
StateResuming
|
||||||
|
StateSessionEstablished
|
||||||
|
StateStreamError
|
||||||
|
StatePermanentError
|
||||||
|
InitialPresence = "<presence/>"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event is a structure use to convey event changes related to client state. This
|
||||||
|
// is for example used to notify the client when the client get disconnected.
|
||||||
|
type Event struct {
|
||||||
|
State SyncConnState
|
||||||
|
Description string
|
||||||
|
StreamError string
|
||||||
|
SMState SMState
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMState holds Stream Management information regarding the session that can be
|
||||||
|
// used to resume session after disconnect
|
||||||
|
type SMState struct {
|
||||||
|
// Stream Management ID
|
||||||
|
Id string
|
||||||
|
// Inbound stanza count
|
||||||
|
Inbound uint
|
||||||
|
|
||||||
|
// IP affinity
|
||||||
|
preferredReconAddr string
|
||||||
|
|
||||||
|
// Error
|
||||||
|
StreamErrorGroup stanza.StanzaErrorGroup
|
||||||
|
|
||||||
|
// Track sent stanzas
|
||||||
|
*stanza.UnAckQueue
|
||||||
|
|
||||||
|
// TODO Store max and timestamp, to check if we should retry resumption or not
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventHandler is use to pass events about state of the connection to
|
||||||
|
// client implementation.
|
||||||
|
type EventHandler func(Event) error
|
||||||
|
|
||||||
|
type EventManager struct {
|
||||||
|
// Store current state. Please use "getState" and "setState" to access and/or modify this.
|
||||||
|
CurrentState SyncConnState
|
||||||
|
|
||||||
|
// Callback used to propagate connection state changes
|
||||||
|
Handler EventHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateState changes the CurrentState in the event manager. The state read is threadsafe but there is no guarantee
|
||||||
|
// regarding the triggered callback function.
|
||||||
|
func (em *EventManager) updateState(state ConnState) {
|
||||||
|
em.CurrentState.setState(state)
|
||||||
|
if em.Handler != nil {
|
||||||
|
em.Handler(Event{State: em.CurrentState})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// disconnected changes the CurrentState in the event manager to "disconnected". The state read is threadsafe but there is no guarantee
|
||||||
|
// regarding the triggered callback function.
|
||||||
|
func (em *EventManager) disconnected(state SMState) {
|
||||||
|
em.CurrentState.setState(StateDisconnected)
|
||||||
|
if em.Handler != nil {
|
||||||
|
em.Handler(Event{State: em.CurrentState, SMState: state})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// streamError changes the CurrentState in the event manager to "streamError". The state read is threadsafe but there is no guarantee
|
||||||
|
// regarding the triggered callback function.
|
||||||
|
func (em *EventManager) streamError(error, desc string) {
|
||||||
|
em.CurrentState.setState(StateStreamError)
|
||||||
|
if em.Handler != nil {
|
||||||
|
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ stanzas")
|
||||||
|
|
||||||
|
// Client is the main structure used to connect as a client on an XMPP
|
||||||
|
// server.
|
||||||
|
type Client struct {
|
||||||
|
// Store user defined options and states
|
||||||
|
config *Config
|
||||||
|
// Session gather data that can be accessed by users of this library
|
||||||
|
Session *Session
|
||||||
|
transport Transport
|
||||||
|
// Router is used to dispatch packets
|
||||||
|
router *Router
|
||||||
|
// Track and broadcast connection state
|
||||||
|
EventManager
|
||||||
|
// Handle errors from client execution
|
||||||
|
ErrorHandler func(error)
|
||||||
|
|
||||||
|
// Post connection hook. This will be executed on first connection
|
||||||
|
PostConnectHook func() error
|
||||||
|
|
||||||
|
// Post resume hook. This will be executed after the client resumes a lost connection using StreamManagement (XEP-0198)
|
||||||
|
PostResumeHook func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Setting up the client / Checking the parameters
|
||||||
|
*/
|
||||||
|
|
||||||
|
// NewClient generates a new XMPP client, based on Config passed as parameters.
|
||||||
|
// If host is not specified, the DNS SRV should be used to find the host from the domain part of the Jid.
|
||||||
|
// Default the port to 5222.
|
||||||
|
func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, err error) {
|
||||||
|
if config.KeepaliveInterval == 0 {
|
||||||
|
config.KeepaliveInterval = time.Second * 30
|
||||||
|
}
|
||||||
|
// Parse Jid
|
||||||
|
if config.parsedJid, err = stanza.NewJid(config.Jid); err != nil {
|
||||||
|
err = errors.New("missing jid")
|
||||||
|
return nil, NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Credential.secret == "" {
|
||||||
|
err = errors.New("missing credential")
|
||||||
|
return nil, NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to jid domain
|
||||||
|
if config.Address == "" {
|
||||||
|
config.Address = config.parsedJid.Domain
|
||||||
|
|
||||||
|
// Fetch SRV DNS-Entries
|
||||||
|
_, srvEntries, err := net.LookupSRV("xmpp-client", "tcp", config.parsedJid.Domain)
|
||||||
|
|
||||||
|
if err == nil && len(srvEntries) > 0 {
|
||||||
|
// If we found matching DNS records, use the entry with highest weight
|
||||||
|
bestSrv := srvEntries[0]
|
||||||
|
for _, srv := range srvEntries {
|
||||||
|
if srv.Priority <= bestSrv.Priority && srv.Weight >= bestSrv.Weight {
|
||||||
|
bestSrv = srv
|
||||||
|
config.Address = ensurePort(srv.Target, int(srv.Port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.Domain == "" {
|
||||||
|
// Fallback to jid domain
|
||||||
|
config.Domain = config.parsedJid.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
c = new(Client)
|
||||||
|
c.config = config
|
||||||
|
c.router = r
|
||||||
|
c.ErrorHandler = errorHandler
|
||||||
|
|
||||||
|
if c.config.ConnectTimeout == 0 {
|
||||||
|
c.config.ConnectTimeout = 15 // 15 second as default
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.TransportConfiguration.Domain == "" {
|
||||||
|
config.TransportConfiguration.Domain = config.parsedJid.Domain
|
||||||
|
}
|
||||||
|
c.config.TransportConfiguration.ConnectTimeout = c.config.ConnectTimeout
|
||||||
|
c.transport = NewClientTransport(c.config.TransportConfiguration)
|
||||||
|
|
||||||
|
if config.StreamLogger != nil {
|
||||||
|
c.transport.LogTraffic(config.StreamLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes a first time connection to a XMPP server.
|
||||||
|
// It calls the PostConnectHook
|
||||||
|
func (c *Client) Connect() error {
|
||||||
|
err := c.connect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO: Do we always want to send initial presence automatically ?
|
||||||
|
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
||||||
|
err = c.sendWithWriter(c.transport, []byte(InitialPresence))
|
||||||
|
// Execute the post first connection hook. Typically this holds "ask for roster" and this type of actions.
|
||||||
|
if c.PostConnectHook != nil {
|
||||||
|
err = c.PostConnectHook()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the keepalive go routine
|
||||||
|
keepaliveQuit := make(chan struct{})
|
||||||
|
go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit)
|
||||||
|
// Start the receiver go routine
|
||||||
|
go c.recv(keepaliveQuit)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect establishes an actual TCP connection, based on previously defined parameters, as well as a XMPP session
|
||||||
|
func (c *Client) connect() error {
|
||||||
|
var state SMState
|
||||||
|
var err error
|
||||||
|
// This is the TCP connection
|
||||||
|
streamId, err := c.transport.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is ok, we now open XMPP session with TLS negotiation if possible and session resume or binding
|
||||||
|
// depending on state.
|
||||||
|
if c.Session, err = NewSession(c, state); err != nil {
|
||||||
|
// Try to get the stream close tag from the server.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
|
if err != nil {
|
||||||
|
c.ErrorHandler(err)
|
||||||
|
c.disconnected(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch val.(type) {
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
|
||||||
|
c.transport.ReceivedStreamClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.Disconnect()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Session.StreamId = streamId
|
||||||
|
c.updateState(StateSessionEstablished)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
||||||
|
// state. See XEP-0198
|
||||||
|
func (c *Client) Resume() error {
|
||||||
|
c.EventManager.updateState(StateResuming)
|
||||||
|
err := c.connect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrieval
|
||||||
|
// for example.
|
||||||
|
if c.PostResumeHook != nil {
|
||||||
|
err = c.PostResumeHook()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect disconnects the client from the server, sending a stream close nonza and closing the TCP connection.
|
||||||
|
func (c *Client) Disconnect() error {
|
||||||
|
if c.transport != nil {
|
||||||
|
return c.transport.Close()
|
||||||
|
}
|
||||||
|
// No transport so no connection.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetHandler(handler EventHandler) {
|
||||||
|
c.Handler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send marshals XMPP stanza and sends it to the server.
|
||||||
|
func (c *Client) Send(packet stanza.Packet) error {
|
||||||
|
conn := c.transport
|
||||||
|
if conn == nil {
|
||||||
|
return errors.New("client is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(packet)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("cannot marshal packet " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store stanza as non-acked as part of stream management
|
||||||
|
// See https://xmpp.org/extensions/xep-0198.html#scenarios
|
||||||
|
if c.config.StreamManagementEnable {
|
||||||
|
if _, ok := packet.(stanza.SMRequest); !ok {
|
||||||
|
toStore := stanza.UnAckedStz{Stz: string(data)}
|
||||||
|
c.Session.SMState.UnAckQueue.Push(&toStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendWithWriter(c.transport, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendIQ sends an IQ set or get stanza to the server. If a result is received
|
||||||
|
// the provided handler function will automatically be called.
|
||||||
|
//
|
||||||
|
// The provided context should have a timeout to prevent the client from waiting
|
||||||
|
// forever for an IQ result. For example:
|
||||||
|
//
|
||||||
|
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||||
|
// result := <- client.SendIQ(ctx, iq)
|
||||||
|
//
|
||||||
|
func (c *Client) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
|
||||||
|
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||||
|
return nil, ErrCanOnlySendGetOrSetIq
|
||||||
|
}
|
||||||
|
if err := c.Send(iq); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRaw sends an XMPP stanza as a string to the server.
|
||||||
|
// It can be invalid XML or XMPP content. In that case, the server will
|
||||||
|
// disconnect the client. It is up to the user of this method to
|
||||||
|
// carefully craft the XML content to produce valid XMPP.
|
||||||
|
func (c *Client) SendRaw(packet string) error {
|
||||||
|
conn := c.transport
|
||||||
|
if conn == nil {
|
||||||
|
return errors.New("client is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store stanza as non-acked as part of stream management
|
||||||
|
// See https://xmpp.org/extensions/xep-0198.html#scenarios
|
||||||
|
if c.config.StreamManagementEnable {
|
||||||
|
toStore := stanza.UnAckedStz{Stz: packet}
|
||||||
|
c.Session.SMState.UnAckQueue.Push(&toStore)
|
||||||
|
}
|
||||||
|
return c.sendWithWriter(c.transport, []byte(packet))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||||
|
var err error
|
||||||
|
_, err = writer.Write(packet)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Go routines
|
||||||
|
|
||||||
|
// Loop: Receive data from server
|
||||||
|
func (c *Client) recv(keepaliveQuit chan<- struct{}) {
|
||||||
|
defer close(keepaliveQuit)
|
||||||
|
|
||||||
|
for {
|
||||||
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
|
if err != nil {
|
||||||
|
c.ErrorHandler(err)
|
||||||
|
c.disconnected(c.Session.SMState)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle stream errors
|
||||||
|
switch packet := val.(type) {
|
||||||
|
case stanza.StreamError:
|
||||||
|
c.router.route(c, val)
|
||||||
|
c.streamError(packet.Error.Local, packet.Text)
|
||||||
|
c.ErrorHandler(errors.New("stream error: " + packet.Error.Local))
|
||||||
|
// We don't return here, because we want to wait for the stream close tag from the server, or timeout.
|
||||||
|
c.Disconnect()
|
||||||
|
// Process Stream management nonzas
|
||||||
|
case stanza.SMRequest:
|
||||||
|
answer := stanza.SMAnswer{XMLName: xml.Name{
|
||||||
|
Space: stanza.NSStreamManagement,
|
||||||
|
Local: "a",
|
||||||
|
}, H: c.Session.SMState.Inbound}
|
||||||
|
err = c.Send(answer)
|
||||||
|
if err != nil {
|
||||||
|
c.ErrorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
|
||||||
|
c.transport.ReceivedStreamClose()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
c.Session.SMState.Inbound++
|
||||||
|
}
|
||||||
|
// Do normal route processing in a go-routine so we can immediately
|
||||||
|
// start receiving other stanzas. This also allows route handlers to
|
||||||
|
// send and receive more stanzas.
|
||||||
|
go c.router.route(c, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop: send whitespace keepalive to server
|
||||||
|
// This is use to keep the connection open, but also to detect connection loss
|
||||||
|
// and trigger proper client connection shutdown.
|
||||||
|
func keepalive(transport Transport, interval time.Duration, quit <-chan struct{}) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := transport.Ping(); err != nil {
|
||||||
|
// When keepalive fails, we force close the transport. In all cases, the recv will also fail.
|
||||||
|
ticker.Stop()
|
||||||
|
_ = transport.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-quit:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
608
client_internal_test.go
Normal file
608
client_internal_test.go
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
streamManagementID = "test-stream_management-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_Send(t *testing.T) {
|
||||||
|
buffer := bytes.NewBufferString("")
|
||||||
|
client := Client{}
|
||||||
|
data := []byte("https://da.wikipedia.org/wiki/J%C3%A6vnd%C3%B8gn")
|
||||||
|
if err := client.sendWithWriter(buffer, data); err != nil {
|
||||||
|
t.Errorf("Writing failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.String() != string(data) {
|
||||||
|
t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream management test.
|
||||||
|
// Connection is established, then the server sends supported features and so on.
|
||||||
|
// After the bind, client attempts a stream management enablement, and server replies in kind.
|
||||||
|
func Test_StreamManagement(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
|
||||||
|
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
enableStreamManagement(t, sc, false, true)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
}, testClientStreamManagement, true, true)
|
||||||
|
go func() {
|
||||||
|
var state SMState
|
||||||
|
var err error
|
||||||
|
// Client is ok, we now open XMPP session
|
||||||
|
if client.Session, err = NewSession(client, state); err != nil {
|
||||||
|
t.Fatalf("failed to open XMPP session: %s", err)
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absence of stream management test.
|
||||||
|
// Connection is established, then the server sends supported features and so on.
|
||||||
|
// Client has stream management disabled in its config, and should not ask for it. Server is not set up to reply.
|
||||||
|
func Test_NoStreamManagement(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
|
||||||
|
// Setup Mock server
|
||||||
|
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
}, testClientStreamManagement, true, false)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var state SMState
|
||||||
|
|
||||||
|
// Client is ok, we now open XMPP session
|
||||||
|
var err error
|
||||||
|
if client.Session, err = NewSession(client, state); err != nil {
|
||||||
|
t.Fatalf("failed to open XMPP session: %s", err)
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_StreamManagementNotSupported(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
|
||||||
|
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
}, testClientStreamManagement, true, true)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var state SMState
|
||||||
|
var err error
|
||||||
|
// Client is ok, we now open XMPP session
|
||||||
|
if client.Session, err = NewSession(client, state); err != nil {
|
||||||
|
t.Fatalf("failed to open XMPP session: %s", err)
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for client
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
|
||||||
|
// Check if client got a positive stream management response from the server
|
||||||
|
if client.Session.Features.DoesStreamManagement() {
|
||||||
|
t.Fatalf("server does not provide stream management")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for server
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_StreamManagementNoResume(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
|
||||||
|
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
enableStreamManagement(t, sc, false, false)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
}, testClientStreamManagement, true, true)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var state SMState
|
||||||
|
var err error
|
||||||
|
// Client is ok, we now open XMPP session
|
||||||
|
if client.Session, err = NewSession(client, state); err != nil {
|
||||||
|
t.Fatalf("failed to open XMPP session: %s", err)
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
if IsStreamResumable(client) {
|
||||||
|
t.Fatalf("server does not support resumption but client says stream is resumable")
|
||||||
|
}
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_StreamManagementResume(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
enableStreamManagement(t, sc, false, true)
|
||||||
|
discardPresence(t, sc)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true,
|
||||||
|
StreamManagementEnable: true,
|
||||||
|
streamManagementResume: true} // Enable stream management
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
router := NewRouter()
|
||||||
|
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Connect client, then disconnect it so we can resume the session
|
||||||
|
go func() {
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not connect client to mock server: %s", err)
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
// Check that the client correctly went into "disconnected" state, after being disconnected
|
||||||
|
statusCorrectChan := make(chan struct{})
|
||||||
|
kill := make(chan struct{})
|
||||||
|
|
||||||
|
transp, ok := client.transport.(*XMPPTransport)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("problem with client transport ")
|
||||||
|
}
|
||||||
|
|
||||||
|
transp.conn.Close()
|
||||||
|
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
mock.Stop()
|
||||||
|
|
||||||
|
go checkClientResumeStatus(client, statusCorrectChan, kill)
|
||||||
|
select {
|
||||||
|
case <-statusCorrectChan:
|
||||||
|
// Test passed
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
kill <- struct{}{}
|
||||||
|
t.Fatalf("Client is not in disconnected state while it should be. Timed out")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the client can have its connection resumed using its state but also its configuration
|
||||||
|
if !IsStreamResumable(client) {
|
||||||
|
t.Fatalf("should support resumption")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
|
||||||
|
// and they should be different between a first connection and a stream resume since exchanged messages
|
||||||
|
// are different (See XEP-0198)
|
||||||
|
mock2 := ServerMock{}
|
||||||
|
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
// Reconnect
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
resumeStream(t, sc)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reconnect
|
||||||
|
go func() {
|
||||||
|
err = client.Resume()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not connect client to mock server: %s", err)
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
|
||||||
|
mock2.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_StreamManagementFail(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
enableStreamManagement(t, sc, true, true)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true,
|
||||||
|
StreamManagementEnable: true,
|
||||||
|
streamManagementResume: true} // Enable stream management
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
router := NewRouter()
|
||||||
|
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var state SMState
|
||||||
|
go func() {
|
||||||
|
_, err = client.transport.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is ok, we now open XMPP session
|
||||||
|
if client.Session, err = NewSession(client, state); err == nil {
|
||||||
|
t.Fatalf("test is supposed to err")
|
||||||
|
}
|
||||||
|
if client.Session.SMState.StreamErrorGroup == nil {
|
||||||
|
t.Fatalf("error was not stored correctly in session state")
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SendStanzaQueueWithSM(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
enableStreamManagement(t, sc, false, true)
|
||||||
|
|
||||||
|
// Ignore the initial presence sent to the server by the client so we can move on to the next packet.
|
||||||
|
discardPresence(t, sc)
|
||||||
|
|
||||||
|
// Used here to silently discard the IQ sent by the client, in order to later trigger a resend
|
||||||
|
skipPacket(t, sc)
|
||||||
|
// Respond to the client ACK request with a number of processed stanzas of 0. This should trigger a resend
|
||||||
|
// of previously ignored stanza to the server, which this handler element will be expecting.
|
||||||
|
respondWithAck(t, sc, 0)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true,
|
||||||
|
StreamManagementEnable: true,
|
||||||
|
streamManagementResume: true} // Enable stream management
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
router := NewRouter()
|
||||||
|
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err = client.Connect()
|
||||||
|
|
||||||
|
client.SendRaw(`<iq id='ls72g593' type='get'>
|
||||||
|
<query xmlns='jabber:iq:roster'/>
|
||||||
|
</iq>
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Last stanza was discarded silently by the server. Let's ask an ack for it. This should trigger resend as the server
|
||||||
|
// will respond with an acknowledged number of stanzas of 0.
|
||||||
|
r := stanza.SMRequest{}
|
||||||
|
client.Send(r)
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
//========================================================================
|
||||||
|
// Helper functions for tests
|
||||||
|
|
||||||
|
func skipPacket(t *testing.T, sc *ServerConn) {
|
||||||
|
var p stanza.IQ
|
||||||
|
se, err := stanza.NextStart(sc.decoder)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot read packet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
|
||||||
|
t.Fatalf("cannot decode packet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondWithAck(t *testing.T, sc *ServerConn, h int) {
|
||||||
|
|
||||||
|
// Mock server reads the ack request
|
||||||
|
var p stanza.SMRequest
|
||||||
|
se, err := stanza.NextStart(sc.decoder)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot read packet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
|
||||||
|
t.Fatalf("cannot decode packet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock server sends the ack response
|
||||||
|
a := stanza.SMAnswer{
|
||||||
|
H: uint(h),
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(a)
|
||||||
|
_, err = sc.connection.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to send response ack")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock server reads the re-sent stanza that was previously discarded intentionally
|
||||||
|
var p2 stanza.IQ
|
||||||
|
nse, err := stanza.NextStart(sc.decoder)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot read packet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sc.decoder.DecodeElement(&p2, &nse); err != nil {
|
||||||
|
t.Fatalf("cannot decode packet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendFeaturesStreamManagment(t *testing.T, sc *ServerConn) {
|
||||||
|
// This is a basic server, supporting only 2 features after auth: stream management & session binding
|
||||||
|
features := `<stream:features>
|
||||||
|
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||||
|
<sm xmlns='urn:xmpp:sm:3'/>
|
||||||
|
</stream:features>`
|
||||||
|
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||||
|
t.Fatalf("cannot send stream feature: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendFeaturesNoStreamManagment(t *testing.T, sc *ServerConn) {
|
||||||
|
// This is a basic server, supporting only 2 features after auth: stream management & session binding
|
||||||
|
features := `<stream:features>
|
||||||
|
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||||
|
</stream:features>`
|
||||||
|
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||||
|
t.Fatalf("cannot send stream feature: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableStreamManagement is a function for the mock server that can either mock a successful session, or fail depending on
|
||||||
|
// the value of the "fail" boolean. True means the session should fail.
|
||||||
|
func enableStreamManagement(t *testing.T, sc *ServerConn, fail bool, resume bool) {
|
||||||
|
// Decode element into pointer storage
|
||||||
|
var ed stanza.SMEnable
|
||||||
|
se, err := stanza.NextStart(sc.decoder)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot read stream management enable: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sc.decoder.DecodeElement(&ed, &se); err != nil {
|
||||||
|
t.Fatalf("cannot decode stream management enable: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fail {
|
||||||
|
f := stanza.SMFailed{
|
||||||
|
H: nil,
|
||||||
|
StreamErrorGroup: &stanza.UnexpectedRequest{},
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshall error response: %s", err)
|
||||||
|
}
|
||||||
|
sc.connection.Write(data)
|
||||||
|
} else {
|
||||||
|
e := &stanza.SMEnabled{
|
||||||
|
Resume: strconv.FormatBool(resume),
|
||||||
|
Id: streamManagementID,
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshall error response: %s", err)
|
||||||
|
}
|
||||||
|
sc.connection.Write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resumeStream(t *testing.T, sc *ServerConn) {
|
||||||
|
h := uint(0)
|
||||||
|
response := stanza.SMResumed{
|
||||||
|
PrevId: streamManagementID,
|
||||||
|
H: &h,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshall stream management enabled response : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
writtenChan := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sc.connection.Write(data)
|
||||||
|
writtenChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-writtenChan:
|
||||||
|
// We're done here
|
||||||
|
return
|
||||||
|
case <-time.After(defaultTimeout):
|
||||||
|
t.Fatalf("failed to write enabled nonza to client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkClientResumeStatus(client *Client, statusCorrectChan chan struct{}, killChan chan struct{}) {
|
||||||
|
for {
|
||||||
|
if client.CurrentState.getState() == StateDisconnected {
|
||||||
|
statusCorrectChan <- struct{}{}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-killChan:
|
||||||
|
return
|
||||||
|
case <-time.After(time.Millisecond * 10):
|
||||||
|
// Keep checking status value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSrvCliForResumeTests(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int, StreamManagementEnable, StreamManagementResume bool) (*Client, *ServerMock) {
|
||||||
|
mock := &ServerMock{}
|
||||||
|
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
|
||||||
|
|
||||||
|
mock.Start(t, testServerAddress, serverHandler)
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testServerAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true,
|
||||||
|
StreamManagementEnable: StreamManagementEnable,
|
||||||
|
streamManagementResume: StreamManagementResume}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Fatalf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = client.transport.Connect(); err != nil {
|
||||||
|
t.Fatalf("XMPP connection failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForEntity(t *testing.T, entityDone chan struct{}) {
|
||||||
|
select {
|
||||||
|
case <-entityDone:
|
||||||
|
case <-time.After(defaultTimeout):
|
||||||
|
t.Fatalf("test timed out")
|
||||||
|
}
|
||||||
|
}
|
659
client_test.go
Normal file
659
client_test.go
Normal file
@ -0,0 +1,659 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Default port is not standard XMPP port to avoid interfering
|
||||||
|
// with local running XMPP server
|
||||||
|
testXMPPAddress = "localhost:15222"
|
||||||
|
testClientDomain = "localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEventManager(t *testing.T) {
|
||||||
|
mgr := EventManager{}
|
||||||
|
mgr.updateState(StateResuming)
|
||||||
|
if mgr.CurrentState.getState() != StateResuming {
|
||||||
|
t.Fatal("CurrentState not updated by updateState()")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.disconnected(SMState{})
|
||||||
|
|
||||||
|
if mgr.CurrentState.getState() != StateDisconnected {
|
||||||
|
t.Fatalf("CurrentState not reset by disconnected()")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.streamError(ErrTLSNotSupported.Error(), "")
|
||||||
|
|
||||||
|
if mgr.CurrentState.getState() != StateStreamError {
|
||||||
|
t.Fatalf("CurrentState not set by streamError()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_Connect(t *testing.T) {
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, handlerClientConnectSuccess)
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
t.Errorf("XMPP connection failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_NoInsecure(t *testing.T) {
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerAbortTLS(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("cannot create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Connect(); err == nil {
|
||||||
|
// When insecure is not allowed:
|
||||||
|
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the client is properly tracking features, as session negotiation progresses.
|
||||||
|
func TestClient_FeaturesTracking(t *testing.T) {
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerAbortTLS(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("cannot create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Connect(); err == nil {
|
||||||
|
// When insecure is not allowed:
|
||||||
|
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_RFC3921Session(t *testing.T) {
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, handlerClientConnectWithSession)
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
t.Errorf("XMPP connection failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testing sending an IQ to the mock server and reading its response.
|
||||||
|
func TestClient_SendIQ(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for Mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
discardPresence(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
client, mock := mockClientConnection(t, h, testClientIqPort)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create the IQ request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
|
||||||
|
// Handle a possible error
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
client.ErrorHandler = errorHandler
|
||||||
|
res, err := client.SendIQ(ctx, iqReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-res: // If the server responds with an IQ, we pass the test
|
||||||
|
case err := <-errChan: // If the server sends an error, or there is a connection error
|
||||||
|
cancel()
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||||
|
cancel()
|
||||||
|
t.Fatal("Failed to receive response, to sent IQ, from mock server")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
mock.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
cancel()
|
||||||
|
t.Fatal("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendIQFail(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for Mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
discardPresence(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
client, mock := mockClientConnection(t, h, testClientIqFailPort)
|
||||||
|
|
||||||
|
//==================
|
||||||
|
// Create an IQ to send
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ request: %v", err)
|
||||||
|
}
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
// Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified
|
||||||
|
// so we need to overwrite it.
|
||||||
|
iqReq.Id = ""
|
||||||
|
|
||||||
|
// Handle a possible error
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
client.ErrorHandler = errorHandler
|
||||||
|
res, _ := client.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
select {
|
||||||
|
case <-res: // If the server responds with an IQ
|
||||||
|
t.Errorf("Server should not respond with an IQ since the request is expected to be invalid !")
|
||||||
|
case <-errChan: // If the server sends an error, the test passes
|
||||||
|
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||||
|
t.Errorf("Failed to receive response, to sent IQ, from mock server")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
mock.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
cancel()
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendRaw(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for Mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
discardPresence(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
type testCase struct {
|
||||||
|
req string
|
||||||
|
shouldErr bool
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
testRequests := make(map[string]testCase)
|
||||||
|
// Sending a correct IQ of type get. Not supposed to err
|
||||||
|
testRequests["Correct IQ"] = testCase{
|
||||||
|
req: `<iq type="get" id="91bd0bba-012f-4d92-bb17-5fc41e6fe545" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: false,
|
||||||
|
port: testClientRawPort + 100,
|
||||||
|
}
|
||||||
|
// Sending an IQ with a missing ID. Should err
|
||||||
|
testRequests["IQ with missing ID"] = testCase{
|
||||||
|
req: `<iq type="get" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: true,
|
||||||
|
port: testClientRawPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A handler for the client.
|
||||||
|
// In the failing test, the server returns a stream error, which triggers this handler, client side.
|
||||||
|
errChan := make(chan error)
|
||||||
|
errHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for all the IQs
|
||||||
|
for name, tcase := range testRequests {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockClientConnection(t, h, tcase.port)
|
||||||
|
c.ErrorHandler = errHandler
|
||||||
|
// Sending raw xml from test case
|
||||||
|
err := c.SendRaw(tcase.req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error sending Raw string")
|
||||||
|
}
|
||||||
|
// Just wait a little so the message has time to arrive
|
||||||
|
select {
|
||||||
|
// We don't use the default "long" timeout here because waiting it out means passing the test.
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
c.Disconnect()
|
||||||
|
case err = <-errChan:
|
||||||
|
if err == nil && tcase.shouldErr {
|
||||||
|
t.Errorf("Failed to get closing stream err")
|
||||||
|
} else if err != nil && !tcase.shouldErr {
|
||||||
|
t.Errorf("This test is not supposed to err !")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
m.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_Disconnect(t *testing.T) {
|
||||||
|
c, m := mockClientConnection(t, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
}, testClientBasePort)
|
||||||
|
err := c.transport.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not ping but not disconnected yet")
|
||||||
|
}
|
||||||
|
c.Disconnect()
|
||||||
|
err = c.transport.Ping()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Did not disconnect properly")
|
||||||
|
}
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DisconnectStreamManager(t *testing.T) {
|
||||||
|
// Init mock server
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerAbortTLS(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("cannot create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sman := NewStreamManager(client, nil)
|
||||||
|
errChan := make(chan error)
|
||||||
|
runSMan := func(errChan chan error) {
|
||||||
|
errChan <- sman.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
go runSMan(errChan)
|
||||||
|
select {
|
||||||
|
case <-errChan:
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
// When insecure is not allowed:
|
||||||
|
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
|
||||||
|
}
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ClientPostConnectHook(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for Mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
hookChan := make(chan struct{})
|
||||||
|
mock := &ServerMock{}
|
||||||
|
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, testClientPostConnectHook)
|
||||||
|
|
||||||
|
mock.Start(t, testServerAddress, h)
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testServerAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The post connection client hook should just write to a channel that we will read later.
|
||||||
|
client.PostConnectHook = func() error {
|
||||||
|
go func() {
|
||||||
|
hookChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Handle a possible error
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
client.ErrorHandler = errorHandler
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
t.Errorf("XMPP connection failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the post connection client hook was correctly called
|
||||||
|
select {
|
||||||
|
case err := <-errChan: // If the server sends an error, or there is a connection error
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||||
|
t.Fatal("Failed to call post connection client hook")
|
||||||
|
case <-hookChan:
|
||||||
|
// Test succeeded, channel was written to.
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
mock.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
t.Fatal("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ClientPostReconnectHook(t *testing.T) {
|
||||||
|
hookChan := make(chan struct{})
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
enableStreamManagement(t, sc, false, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true,
|
||||||
|
StreamManagementEnable: true,
|
||||||
|
streamManagementResume: true} // Enable stream management
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
router := NewRouter()
|
||||||
|
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.PostResumeHook = func() error {
|
||||||
|
go func() {
|
||||||
|
hookChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not connect client to mock server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transp, ok := client.transport.(*XMPPTransport)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("problem with client transport ")
|
||||||
|
}
|
||||||
|
|
||||||
|
transp.conn.Close()
|
||||||
|
mock.Stop()
|
||||||
|
|
||||||
|
// Check if the client can have its connection resumed using its state but also its configuration
|
||||||
|
if !IsStreamResumable(client) {
|
||||||
|
t.Fatalf("should support resumption")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
|
||||||
|
// and they should be different between a first connection and a stream resume since exchanged messages
|
||||||
|
// are different (See XEP-0198)
|
||||||
|
mock2 := ServerMock{}
|
||||||
|
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
// Reconnect
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||||
|
resumeStream(t, sc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reconnect
|
||||||
|
err = client.Resume()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not connect client to mock server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||||
|
t.Fatal("Failed to call post connection client hook")
|
||||||
|
case <-hookChan:
|
||||||
|
// Test succeeded, channel was written to.
|
||||||
|
}
|
||||||
|
|
||||||
|
mock2.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Basic XMPP Server Mock Handlers.
|
||||||
|
|
||||||
|
// Test connection with a basic straightforward workflow
|
||||||
|
func handlerClientConnectSuccess(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendBindFeature(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeConn closes the connection on request from the client
|
||||||
|
func closeConn(t *testing.T, sc *ServerConn) {
|
||||||
|
for {
|
||||||
|
cls, err := stanza.NextPacket(sc.decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read from socket: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch cls.(type) {
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
sc.connection.Write([]byte(stanza.StreamClose))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect client will abort on TLS
|
||||||
|
func handlerAbortTLS(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test connection with mandatory session (RFC-3921)
|
||||||
|
func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) {
|
||||||
|
checkClientOpenStream(t, sc)
|
||||||
|
|
||||||
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
|
sendRFC3921Feature(t, sc) // Send post auth features
|
||||||
|
bind(t, sc)
|
||||||
|
session(t, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkClientOpenStream(t *testing.T, sc *ServerConn) {
|
||||||
|
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to set deadline: %v", err)
|
||||||
|
}
|
||||||
|
defer sc.connection.SetDeadline(time.Time{})
|
||||||
|
|
||||||
|
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
|
||||||
|
var token xml.Token
|
||||||
|
token, err := sc.decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot read next token: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch elem := token.(type) {
|
||||||
|
// Wait for first startElement
|
||||||
|
case xml.StartElement:
|
||||||
|
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
|
||||||
|
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(sc.connection, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
|
||||||
|
t.Errorf("cannot write server stream open: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int) (*Client, *ServerMock) {
|
||||||
|
mock := &ServerMock{}
|
||||||
|
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
|
||||||
|
|
||||||
|
mock.Start(t, testServerAddress, serverHandler)
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testServerAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
t.Errorf("XMPP connection failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// This really should not be used as is.
|
||||||
|
// It's just meant to be a placeholder when error handling is not needed at this level
|
||||||
|
func clientDefaultErrorHandler(err error) {
|
||||||
|
}
|
198
cmd/fluuxmpp/README.md
Normal file
198
cmd/fluuxmpp/README.md
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
# fluuxmpp
|
||||||
|
|
||||||
|
fluuxIO's XMPP command-line tool
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install `fluuxmpp` in your Go path:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get -u gosrc.io/xmpp/cmd/fluuxmpp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp --help
|
||||||
|
fluuxIO's xmpp comandline tool
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
fluuxmpp [command]
|
||||||
|
|
||||||
|
Available Commands:
|
||||||
|
check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
|
||||||
|
help Help about any command
|
||||||
|
send is a command-line tool to send to send XMPP messages to users
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for fluuxmpp
|
||||||
|
|
||||||
|
Use "fluuxmpp [command] --help" for more information about a command.
|
||||||
|
```
|
||||||
|
|
||||||
|
### check tls
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp check --help
|
||||||
|
is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
fluuxmpp check <host[:port]> [flags]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-d, --domain string domain if host handle multiple domains
|
||||||
|
-h, --help help for check
|
||||||
|
```
|
||||||
|
|
||||||
|
### sending messages
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp send --help
|
||||||
|
is a command-line tool to send to send XMPP messages to users
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
fluuxmpp send <recipient,> [message] [flags]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
fluuxmpp send to@chat.sum7.eu "Hello World!"
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--addr string host[:port]
|
||||||
|
--config string config file (default is ~/.config/fluuxmpp.yml)
|
||||||
|
-h, --help help for send
|
||||||
|
--jid string using jid (required)
|
||||||
|
-m, --muc recipient is a muc (join it before sending messages)
|
||||||
|
--password string using password for your jid (required)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### check tls
|
||||||
|
|
||||||
|
If you server is on standard port and XMPP domains matches the hostname you can simply use:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp check chat.sum7.eu
|
||||||
|
info All checks passed
|
||||||
|
⇢ address="chat.sum7.eu" domain=""
|
||||||
|
⇢ main.go:43 main.runCheck
|
||||||
|
⇢ 2019-07-16T22:01:39.765+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass the port and the XMPP domain if different from the server hostname:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de
|
||||||
|
info All checks passed
|
||||||
|
⇢ address="chat.sum7.eu:5222" domain="meckerspace.de"
|
||||||
|
⇢ main.go:43 main.runCheck
|
||||||
|
⇢ 2019-07-16T22:01:33.270+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Error code will be non-zero in case of error. You can thus use it directly with your usual
|
||||||
|
monitoring scripts.
|
||||||
|
|
||||||
|
|
||||||
|
### sending messages
|
||||||
|
|
||||||
|
Message from arguments:
|
||||||
|
```bash
|
||||||
|
$ fluuxmpp send to@example.org "Hello World!"
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:42:43.310+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Hello World!" to="to@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:42:43.310+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Message from STDIN:
|
||||||
|
```bash
|
||||||
|
$ journalctl -f | fluuxmpp send to@example.org -
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:40:03.177+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="to@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:40:03.178+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Jul 17 23:36:46 RECHNERNAME systemd[755]: Started Fetch mails." to="to@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:40:03.178+02:00
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Multiple recipients:
|
||||||
|
```bash
|
||||||
|
$ fluuxmpp send to1@example.org,to2@example.org "Multiple recipient"
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:47:57.650+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Multiple recipient" to="to1@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:47:57.651+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Multiple recipient" to="to2@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:47:57.652+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Send to MUC:
|
||||||
|
```bash
|
||||||
|
journalctl -f | fluuxmpp send testit@conference.chat.sum7.eu - --muc
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:52:56.269+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=true text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="testit@conference.chat.sum7.eu"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:52:56.270+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=true text="Jul 17 23:48:58 RECHNERNAME systemd[755]: mail.service: Succeeded." to="testit@conference.chat.sum7.eu"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:52:56.277+02:00
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentification
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
|
In `/etc/`, `~/.config` and `.` (here).
|
||||||
|
You could create the file name `fluuxmpp` with you favorite file extension (e.g. `toml`, `yml`).
|
||||||
|
|
||||||
|
e.g. ~/.config/fluuxmpp.toml
|
||||||
|
```toml
|
||||||
|
jid = "bot@example.org"
|
||||||
|
password = "secret"
|
||||||
|
|
||||||
|
addr = "example.com:5222"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FLUXXMPP_JID='bot@example.org';
|
||||||
|
export FLUXXMPP_PASSWORD='secret';
|
||||||
|
|
||||||
|
export FLUXXMPP_ADDR='example.com:5222';
|
||||||
|
|
||||||
|
fluuxmpp send to@example.org "Hello Welt";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Warning: This should not be used for production systems, as all users on the system
|
||||||
|
can read the running processes, and their parameters (and thus the password).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fluuxmpp send to@example.org "Hello World!" --jid bot@example.org --password secret --addr example.com:5222;
|
||||||
|
```
|
21
cmd/fluuxmpp/TODO.md
Normal file
21
cmd/fluuxmpp/TODO.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## check
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Use a config file to define the checks to perform as client on an XMPP server.
|
||||||
|
|
||||||
|
## send
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
- Remove global variable (like mucToleave)
|
||||||
|
- Does not report error when trying to connect to a non open port (for example localhost with no server running).
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- configuration
|
||||||
|
- allow unencrypted
|
||||||
|
- skip tls verification
|
||||||
|
- support muc and single user at same time
|
||||||
|
- send html -> parse console colors to xhtml (is there a easy way or lib for it ?)
|
41
cmd/fluuxmpp/check.go
Normal file
41
cmd/fluuxmpp/check.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var domain = ""
|
||||||
|
var cmdCheck = &cobra.Command{
|
||||||
|
Use: "check <host[:port]>",
|
||||||
|
Short: "is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires",
|
||||||
|
Example: "fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
runCheck(args[0], domain)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdRoot.AddCommand(cmdCheck)
|
||||||
|
cmdCheck.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCheck(address, domain string) {
|
||||||
|
logger := log.WithFields(map[string]interface{}{
|
||||||
|
"address": address,
|
||||||
|
"domain": domain,
|
||||||
|
})
|
||||||
|
client, err := xmpp.NewChecker(address, domain)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Check(); err != nil {
|
||||||
|
logger.Fatal("Failed connection check: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Println("All checks passed")
|
||||||
|
}
|
5
cmd/fluuxmpp/doc.go
Normal file
5
cmd/fluuxmpp/doc.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
fluuxmpp: fluuxIO's xmpp comandline tool
|
||||||
|
*/
|
||||||
|
package main
|
34
cmd/fluuxmpp/log.go
Normal file
34
cmd/fluuxmpp/log.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
stdLogger "github.com/bdlm/std/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hook struct{}
|
||||||
|
|
||||||
|
func (h *hook) Fire(entry *log.Entry) error {
|
||||||
|
switch entry.Level {
|
||||||
|
case log.PanicLevel:
|
||||||
|
entry.Logger.Out = os.Stderr
|
||||||
|
case log.FatalLevel:
|
||||||
|
entry.Logger.Out = os.Stderr
|
||||||
|
case log.ErrorLevel:
|
||||||
|
entry.Logger.Out = os.Stderr
|
||||||
|
case log.WarnLevel:
|
||||||
|
entry.Logger.Out = os.Stdout
|
||||||
|
case log.InfoLevel:
|
||||||
|
entry.Logger.Out = os.Stdout
|
||||||
|
case log.DebugLevel:
|
||||||
|
entry.Logger.Out = os.Stdout
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hook) Levels() []stdLogger.Level {
|
||||||
|
return log.AllLevels
|
||||||
|
}
|
19
cmd/fluuxmpp/main.go
Normal file
19
cmd/fluuxmpp/main.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmdRoot represents the base command when called without any subcommands
|
||||||
|
var cmdRoot = &cobra.Command{
|
||||||
|
Use: "fluuxmpp",
|
||||||
|
Short: "fluuxIO's xmpp comandline tool",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.AddHook(&hook{})
|
||||||
|
if err := cmdRoot.Execute(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
141
cmd/fluuxmpp/send.go
Normal file
141
cmd/fluuxmpp/send.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configFile = ""
|
||||||
|
|
||||||
|
// FIXME: Remove global variables
|
||||||
|
var isMUCRecipient = false
|
||||||
|
|
||||||
|
var cmdSend = &cobra.Command{
|
||||||
|
Use: "send <recipient,> [message]",
|
||||||
|
Short: "is a command-line tool to send to send XMPP messages to users",
|
||||||
|
Example: `fluuxmpp send to@chat.sum7.eu "Hello World!"`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: sendxmpp,
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendxmpp(cmd *cobra.Command, args []string) {
|
||||||
|
receiver := strings.Split(args[0], ",")
|
||||||
|
msgText := args[1]
|
||||||
|
|
||||||
|
var err error
|
||||||
|
client, err := xmpp.NewClient(&xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: viper.GetString("addr"),
|
||||||
|
},
|
||||||
|
Jid: viper.GetString("jid"),
|
||||||
|
Credential: xmpp.Password(viper.GetString("password")),
|
||||||
|
},
|
||||||
|
xmpp.NewRouter(),
|
||||||
|
func(err error) {
|
||||||
|
log.Println(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error when starting xmpp client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
// FIXME: Remove global variables
|
||||||
|
var mucsToLeave []*stanza.Jid
|
||||||
|
|
||||||
|
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
log.Info("client connected")
|
||||||
|
|
||||||
|
if isMUCRecipient {
|
||||||
|
for _, muc := range receiver {
|
||||||
|
jid, err := stanza.NewJid(muc)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
jid.Resource = "sendxmpp"
|
||||||
|
|
||||||
|
if err := joinMUC(c, jid); err != nil {
|
||||||
|
log.WithField("muc", muc).Errorf("error joining muc: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mucsToLeave = append(mucsToLeave, jid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if msgText != "-" {
|
||||||
|
send(c, receiver, msgText)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
send(c, receiver, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Errorf("error on reading stdin: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
err := cm.Run()
|
||||||
|
log.Panic("closed connection:", err)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
leaveMUCs(client, mucsToLeave)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdRoot.AddCommand(cmdSend)
|
||||||
|
|
||||||
|
cobra.OnInitialize(initConfigFile)
|
||||||
|
cmdSend.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ~/.config/fluuxmpp.yml)")
|
||||||
|
|
||||||
|
cmdSend.Flags().StringP("jid", "", "", "using jid (required)")
|
||||||
|
viper.BindPFlag("jid", cmdSend.Flags().Lookup("jid"))
|
||||||
|
|
||||||
|
cmdSend.Flags().StringP("password", "", "", "using password for your jid (required)")
|
||||||
|
viper.BindPFlag("password", cmdSend.Flags().Lookup("password"))
|
||||||
|
|
||||||
|
cmdSend.Flags().StringP("addr", "", "", "host[:port]")
|
||||||
|
viper.BindPFlag("addr", cmdSend.Flags().Lookup("addr"))
|
||||||
|
|
||||||
|
cmdSend.Flags().BoolVarP(&isMUCRecipient, "muc", "m", false, "recipient is a muc (join it before sending messages)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig reads in config file and ENV variables if set.
|
||||||
|
func initConfigFile() {
|
||||||
|
if configFile != "" {
|
||||||
|
viper.SetConfigFile(configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigName("fluuxmpp")
|
||||||
|
viper.AddConfigPath("/etc/")
|
||||||
|
viper.AddConfigPath("$HOME/.config")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
|
||||||
|
viper.SetEnvPrefix("FLUXXMPP")
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
// If a config file is found, read it in.
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
log.Warnf("no configuration found (somebody could read your password from process argument list): %s", err)
|
||||||
|
}
|
||||||
|
}
|
28
cmd/fluuxmpp/xmppmuc.go
Normal file
28
cmd/fluuxmpp/xmppmuc.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func joinMUC(c xmpp.Sender, toJID *stanza.Jid) error {
|
||||||
|
return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()},
|
||||||
|
Extensions: []stanza.PresExtension{
|
||||||
|
stanza.MucPresence{
|
||||||
|
History: stanza.History{MaxStanzas: stanza.NewNullableInt(0)},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func leaveMUCs(c xmpp.Sender, mucsToLeave []*stanza.Jid) {
|
||||||
|
for _, muc := range mucsToLeave {
|
||||||
|
if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{
|
||||||
|
To: muc.Full(),
|
||||||
|
Type: stanza.PresenceTypeUnavailable,
|
||||||
|
}}); err != nil {
|
||||||
|
log.WithField("muc", muc).Errorf("error on leaving muc: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
cmd/fluuxmpp/xmppsend.go
Normal file
36
cmd/fluuxmpp/xmppsend.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func send(c xmpp.Sender, recipient []string, msgText string) {
|
||||||
|
msg := stanza.Message{
|
||||||
|
Attrs: stanza.Attrs{Type: stanza.MessageTypeChat},
|
||||||
|
Body: msgText,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMUCRecipient {
|
||||||
|
msg.Type = stanza.MessageTypeGroupchat
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, to := range recipient {
|
||||||
|
msg.To = to
|
||||||
|
if err := c.Send(msg); err != nil {
|
||||||
|
log.WithFields(map[string]interface{}{
|
||||||
|
"muc": isMUCRecipient,
|
||||||
|
"to": to,
|
||||||
|
"text": msgText,
|
||||||
|
}).Errorf("error on send message: %s", err)
|
||||||
|
} else {
|
||||||
|
log.WithFields(map[string]interface{}{
|
||||||
|
"muc": isMUCRecipient,
|
||||||
|
"to": to,
|
||||||
|
"text": msgText,
|
||||||
|
}).Info("send message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
cmd/go.mod
Normal file
13
cmd/go.mod
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module gosrc.io/xmpp/cmd
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bdlm/log v0.1.19
|
||||||
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7
|
||||||
|
github.com/spf13/cobra v0.0.5
|
||||||
|
github.com/spf13/viper v1.6.1
|
||||||
|
gosrc.io/xmpp v0.1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
replace gosrc.io/xmpp => ./../
|
255
cmd/go.sum
Normal file
255
cmd/go.sum
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
|
||||||
|
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
|
||||||
|
github.com/bdlm/log v0.1.19 h1:GqVFZC+khJCEbtTmkaDL/araNDwxTeLBmdMK8pbRoBE=
|
||||||
|
github.com/bdlm/log v0.1.19/go.mod h1:30V5Zwc5Vt5ePq5rd9KJ6JQ/A5aFUcKzq5fYtO7c9qc=
|
||||||
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 h1:ggZyn+N8eoBh/qLla2kUtqm/ysjnkbzUxTQY+6LMshY=
|
||||||
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7/go.mod h1:E4vIYZDcEPVbE/Dbxc7GpA3YJpXnsF5csRt8LptMGWI=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||||
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||||
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
|
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||||
|
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||||
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
230
component.go
Normal file
230
component.go
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ComponentOptions struct {
|
||||||
|
TransportConfiguration
|
||||||
|
|
||||||
|
// =================================
|
||||||
|
// Component Connection Info
|
||||||
|
|
||||||
|
// Domain is the XMPP server subdomain that the component will handle
|
||||||
|
Domain string
|
||||||
|
// Secret is the "password" used by the XMPP server to secure component access
|
||||||
|
Secret string
|
||||||
|
|
||||||
|
// =================================
|
||||||
|
// Component discovery
|
||||||
|
|
||||||
|
// Component human readable name, that will be shown in XMPP discovery
|
||||||
|
Name string
|
||||||
|
// Typical categories and types: https://xmpp.org/registrar/disco-categories.html
|
||||||
|
Category string
|
||||||
|
Type string
|
||||||
|
|
||||||
|
// =================================
|
||||||
|
// Communication with developer client / StreamManager
|
||||||
|
|
||||||
|
// Track and broadcast connection state
|
||||||
|
EventManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component implements an XMPP extension allowing to extend XMPP server
|
||||||
|
// using external components. Component specifications are defined
|
||||||
|
// in XEP-0114, XEP-0355 and XEP-0356.
|
||||||
|
type Component struct {
|
||||||
|
ComponentOptions
|
||||||
|
router *Router
|
||||||
|
|
||||||
|
transport Transport
|
||||||
|
|
||||||
|
// read / write
|
||||||
|
socketProxy io.ReadWriter // TODO
|
||||||
|
ErrorHandler func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*Component, error) {
|
||||||
|
c := Component{ComponentOptions: opts, router: r, ErrorHandler: errorHandler}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect triggers component connection to XMPP server component port.
|
||||||
|
// TODO: Failed handshake should be a permanent error
|
||||||
|
func (c *Component) Connect() error {
|
||||||
|
return c.Resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) Resume() error {
|
||||||
|
var err error
|
||||||
|
var streamId string
|
||||||
|
if c.ComponentOptions.TransportConfiguration.Domain == "" {
|
||||||
|
c.ComponentOptions.TransportConfiguration.Domain = c.ComponentOptions.Domain
|
||||||
|
}
|
||||||
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
c.updateState(StatePermanentError)
|
||||||
|
return NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamId, err = c.transport.Connect(); err != nil {
|
||||||
|
c.updateState(StatePermanentError)
|
||||||
|
return NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("<handshake>%s</handshake>", c.handshake(streamId)))); err != nil {
|
||||||
|
c.updateState(StateStreamError)
|
||||||
|
|
||||||
|
return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check server response for authentication
|
||||||
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
|
if err != nil {
|
||||||
|
c.updateState(StatePermanentError)
|
||||||
|
return NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case stanza.StreamError:
|
||||||
|
c.streamError("conflict", "no auth loop")
|
||||||
|
return NewConnError(errors.New("handshake failed "+v.Error.Local), true)
|
||||||
|
case stanza.Handshake:
|
||||||
|
// Start the receiver go routine
|
||||||
|
c.updateState(StateSessionEstablished)
|
||||||
|
go c.recv()
|
||||||
|
return err // Should be empty at this point
|
||||||
|
default:
|
||||||
|
c.updateState(StatePermanentError)
|
||||||
|
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) Disconnect() error {
|
||||||
|
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
||||||
|
if c.transport != nil {
|
||||||
|
return c.transport.Close()
|
||||||
|
}
|
||||||
|
// No transport so no connection.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) SetHandler(handler EventHandler) {
|
||||||
|
c.Handler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receiver Go routine receiver
|
||||||
|
func (c *Component) recv() {
|
||||||
|
for {
|
||||||
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
|
if err != nil {
|
||||||
|
c.updateState(StateDisconnected)
|
||||||
|
c.ErrorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Handle stream errors
|
||||||
|
switch p := val.(type) {
|
||||||
|
case stanza.StreamError:
|
||||||
|
c.router.route(c, val)
|
||||||
|
c.streamError(p.Error.Local, p.Text)
|
||||||
|
c.ErrorHandler(errors.New("stream error: " + p.Error.Local))
|
||||||
|
// We don't return here, because we want to wait for the stream close tag from the server, or timeout.
|
||||||
|
c.Disconnect()
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
|
||||||
|
c.transport.ReceivedStreamClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.router.route(c, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send marshalls XMPP stanza and sends it to the server.
|
||||||
|
func (c *Component) Send(packet stanza.Packet) error {
|
||||||
|
transport := c.transport
|
||||||
|
if transport == nil {
|
||||||
|
return errors.New("component is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(packet)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("cannot marshal packet " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.sendWithWriter(transport, data); err != nil {
|
||||||
|
return errors.New("cannot send packet " + err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||||
|
var err error
|
||||||
|
_, err = writer.Write(packet)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendIQ sends an IQ set or get stanza to the server. If a result is received
|
||||||
|
// the provided handler function will automatically be called.
|
||||||
|
//
|
||||||
|
// The provided context should have a timeout to prevent the client from waiting
|
||||||
|
// forever for an IQ result. For example:
|
||||||
|
//
|
||||||
|
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||||
|
// result := <- client.SendIQ(ctx, iq)
|
||||||
|
//
|
||||||
|
func (c *Component) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
|
||||||
|
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||||
|
return nil, ErrCanOnlySendGetOrSetIq
|
||||||
|
}
|
||||||
|
if err := c.Send(iq); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRaw sends an XMPP stanza as a string to the server.
|
||||||
|
// It can be invalid XML or XMPP content. In that case, the server will
|
||||||
|
// disconnect the component. It is up to the user of this method to
|
||||||
|
// carefully craft the XML content to produce valid XMPP.
|
||||||
|
func (c *Component) SendRaw(packet string) error {
|
||||||
|
transport := c.transport
|
||||||
|
if transport == nil {
|
||||||
|
return errors.New("component is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
err = c.sendWithWriter(transport, []byte(packet))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// handshake generates an authentication token based on StreamID and shared secret.
|
||||||
|
func (c *Component) handshake(streamId string) string {
|
||||||
|
// 1. Concatenate the Stream ID received from the server with the shared secret.
|
||||||
|
concatStr := streamId + c.Secret
|
||||||
|
|
||||||
|
// 2. Hash the concatenated string according to the SHA1 algorithm, i.e., SHA1( concat (sid, password)).
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write([]byte(concatStr))
|
||||||
|
hash := h.Sum(nil)
|
||||||
|
|
||||||
|
// 3. Ensure that the hash output is in hexadecimal format, not binary or base64.
|
||||||
|
// 4. Convert the hash output to all lowercase characters.
|
||||||
|
encodedStr := hex.EncodeToString(hash)
|
||||||
|
|
||||||
|
return encodedStr
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: Add support for discovery management directly in component
|
||||||
|
TODO: Support multiple identities on disco info
|
||||||
|
TODO: Support returning features on disco info
|
||||||
|
*/
|
512
component_test.go
Normal file
512
component_test.go
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tests are ran in parallel, so each test creating a server must use a different port so we do not get any
|
||||||
|
// conflict. Using iota for this should do the trick.
|
||||||
|
const (
|
||||||
|
defaultChannelTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandshake(t *testing.T) {
|
||||||
|
opts := ComponentOptions{
|
||||||
|
Domain: "test.localhost",
|
||||||
|
Secret: "mypass",
|
||||||
|
}
|
||||||
|
c := Component{ComponentOptions: opts}
|
||||||
|
|
||||||
|
streamID := "1263952298440005243"
|
||||||
|
expected := "c77e2ef0109fbbc5161e83b51629cd1353495332"
|
||||||
|
|
||||||
|
result := c.handshake(streamID)
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("incorrect handshake calculation '%s' != '%s'", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests connection process with a handshake exchange
|
||||||
|
// Tests multiple session IDs. All serverConnections should generate a unique stream ID
|
||||||
|
func TestGenerateHandshakeId(t *testing.T) {
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
// Using this array with a channel to make a queue of values to test
|
||||||
|
// These are stream IDs that will be used to test the connection process, mixing them with the "secret" to generate
|
||||||
|
// some handshake value
|
||||||
|
var uuidsArray = [5]string{}
|
||||||
|
for i := 1; i < len(uuidsArray); i++ {
|
||||||
|
id, _ := uuid.NewRandom()
|
||||||
|
uuidsArray[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel to pass stream IDs as a queue
|
||||||
|
var uchan = make(chan string, len(uuidsArray))
|
||||||
|
// Populate test channel
|
||||||
|
for _, elt := range uuidsArray {
|
||||||
|
uchan <- elt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a Component connection with a handshake. It expects to have an ID sent its way through the "uchan"
|
||||||
|
// channel of this file. Otherwise it will hang for ever.
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
checkOpenStreamHandshakeID(t, sc, <-uchan)
|
||||||
|
readHandshakeComponent(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<handshake/>")) // That's all the server needs to return (see xep-0114)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddess := fmt.Sprintf("%s:%d", testComponentDomain, testHandshakePort)
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testComponentAddess, h)
|
||||||
|
|
||||||
|
// Init component
|
||||||
|
opts := ComponentOptions{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testComponentAddess,
|
||||||
|
Domain: "localhost",
|
||||||
|
},
|
||||||
|
Domain: testComponentDomain,
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: "Test Component",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
router := NewRouter()
|
||||||
|
c, err := NewComponent(opts, router, componentDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try connecting, and storing the resulting streamID in a map.
|
||||||
|
go func() {
|
||||||
|
m := make(map[string]bool)
|
||||||
|
for range uuidsArray {
|
||||||
|
idChan := make(chan string)
|
||||||
|
go func() {
|
||||||
|
streamId, err := c.transport.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to mock component connection to get a handshake: %s", err)
|
||||||
|
}
|
||||||
|
idChan <- streamId
|
||||||
|
}()
|
||||||
|
|
||||||
|
var streamId string
|
||||||
|
select {
|
||||||
|
case streamId = <-idChan:
|
||||||
|
case <-time.After(defaultTimeout):
|
||||||
|
t.Fatalf("test timed out")
|
||||||
|
}
|
||||||
|
|
||||||
|
hs := stanza.Handshake{
|
||||||
|
Value: c.handshake(streamId),
|
||||||
|
}
|
||||||
|
m[hs.Value] = true
|
||||||
|
hsRaw, err := xml.Marshal(hs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not marshal handshake: %s", err)
|
||||||
|
}
|
||||||
|
c.SendRaw(string(hsRaw))
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
c.transport.Close()
|
||||||
|
}
|
||||||
|
if len(uuidsArray) != len(m) {
|
||||||
|
t.Errorf("Handshake does not produce a unique id. Expected: %d unique ids, got: %d", len(uuidsArray), len(m))
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that NewStreamManager can accept a Component.
|
||||||
|
//
|
||||||
|
// This validates that Component conforms to StreamClient interface.
|
||||||
|
func TestStreamManager(t *testing.T) {
|
||||||
|
NewStreamManager(&Component{}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the decoder is properly initialized when connecting a component to a server.
|
||||||
|
// The decoder is expected to be built after a valid connection
|
||||||
|
// Based on the xmpp_component example.
|
||||||
|
func TestDecoder(t *testing.T) {
|
||||||
|
c, _ := mockComponentConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID)
|
||||||
|
if c.transport.GetDecoder() == nil {
|
||||||
|
t.Errorf("Failed to initialize decoder. Decoder is nil.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests sending an IQ to the server, and getting the response
|
||||||
|
func TestSendIq(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerForComponentIQSend(t, sc)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockComponentConnection(t, testSendIqPort, h)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ request: %v", err)
|
||||||
|
}
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
|
||||||
|
// Handle a possible error
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
c.ErrorHandler = errorHandler
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var res chan stanza.IQ
|
||||||
|
res, _ = c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-res:
|
||||||
|
case err := <-errChan:
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking that error handling is done properly client side when an invalid IQ is sent and the server responds in kind.
|
||||||
|
func TestSendIqFail(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerForComponentIQSend(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockComponentConnection(t, testSendIqFailPort, h)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified
|
||||||
|
// so we need to overwrite it.
|
||||||
|
iqReq.Id = ""
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
c.ErrorHandler = errorHandler
|
||||||
|
|
||||||
|
var res chan stanza.IQ
|
||||||
|
res, _ = c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case r := <-res: // Do we get an IQ response from the server ?
|
||||||
|
t.Errorf("We should not be getting an IQ response here : this should fail !")
|
||||||
|
fmt.Println(r)
|
||||||
|
case <-errChan: // Do we get a stream error from the server ?
|
||||||
|
// If we get an error from the server, the test passes.
|
||||||
|
case <-time.After(defaultChannelTimeout): // Timeout ?
|
||||||
|
t.Errorf("Failed to receive response, to sent IQ, from mock server")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
m.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests sending raw xml to the mock server.
|
||||||
|
// Right now, the server response is not checked and an err is passed in a channel if the test is supposed to err.
|
||||||
|
// In this test, we use IQs
|
||||||
|
func TestSendRaw(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for the mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
// Completes the connection by exchanging handshakes
|
||||||
|
handlerForComponentHandshakeDefaultID(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
req string
|
||||||
|
shouldErr bool
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
testRequests := make(map[string]testCase)
|
||||||
|
// Sending a correct IQ of type get. Not supposed to err
|
||||||
|
testRequests["Correct IQ"] = testCase{
|
||||||
|
req: `<iq type="get" id="91bd0bba-012f-4d92-bb17-5fc41e6fe545" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: false,
|
||||||
|
port: testSendRawPort + 100,
|
||||||
|
}
|
||||||
|
// Sending an IQ with a missing ID. Should err
|
||||||
|
testRequests["IQ with missing ID"] = testCase{
|
||||||
|
req: `<iq type="get" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: true,
|
||||||
|
port: testSendRawPort + 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A handler for the component.
|
||||||
|
// In the failing test, the server returns a stream error, which triggers this handler, component side.
|
||||||
|
errChan := make(chan error)
|
||||||
|
errHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for all the IQs
|
||||||
|
for name, tcase := range testRequests {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockComponentConnection(t, tcase.port, h)
|
||||||
|
c.ErrorHandler = errHandler
|
||||||
|
// Sending raw xml from test case
|
||||||
|
err := c.SendRaw(tcase.req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error sending Raw string")
|
||||||
|
}
|
||||||
|
// Just wait a little so the message has time to arrive
|
||||||
|
select {
|
||||||
|
// We don't use the default "long" timeout here because waiting it out means passing the test.
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
case err = <-errChan:
|
||||||
|
if err == nil && tcase.shouldErr {
|
||||||
|
t.Errorf("Failed to get closing stream err")
|
||||||
|
} else if err != nil && !tcase.shouldErr {
|
||||||
|
t.Errorf("This test is not supposed to err ! => %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.transport.Close()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
m.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests the Disconnect method for Components
|
||||||
|
func TestDisconnect(t *testing.T) {
|
||||||
|
c, m := mockComponentConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID)
|
||||||
|
err := c.transport.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not ping but not disconnected yet")
|
||||||
|
}
|
||||||
|
c.Disconnect()
|
||||||
|
err = c.transport.Ping()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Did not disconnect properly")
|
||||||
|
}
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that a streamManager successfully disconnects when a handshake fails between the component and the server.
|
||||||
|
func TestStreamManagerDisconnect(t *testing.T) {
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, testSManDisconnectPort)
|
||||||
|
mock := ServerMock{}
|
||||||
|
// Handler fails the handshake, which is currently the only option to disconnect completely when using a streamManager
|
||||||
|
// a failed handshake being a permanent error, except for a "conflict"
|
||||||
|
mock.Start(t, testComponentAddress, handlerComponentFailedHandshakeDefaultID)
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// Create Component to connect to it
|
||||||
|
c := makeBasicComponent(defaultComponentName, testComponentAddress, t)
|
||||||
|
|
||||||
|
//========================================
|
||||||
|
// Connect the new Component to the server
|
||||||
|
cm := NewStreamManager(c, nil)
|
||||||
|
errChan := make(chan error)
|
||||||
|
runSMan := func(errChan chan error) {
|
||||||
|
errChan <- cm.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
go runSMan(errChan)
|
||||||
|
select {
|
||||||
|
case <-errChan:
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("The component and server seem to still be connected while they should not.")
|
||||||
|
}
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Basic XMPP Server Mock Handlers.
|
||||||
|
|
||||||
|
//===============================
|
||||||
|
// Init mock server and connection
|
||||||
|
// Creating a mock server and connecting a Component to it. Initialized with given port and handler function
|
||||||
|
// The Component and mock are both returned
|
||||||
|
func mockComponentConnection(t *testing.T, port int, handler func(t *testing.T, sc *ServerConn)) (*Component, *ServerMock) {
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port)
|
||||||
|
mock := &ServerMock{}
|
||||||
|
mock.Start(t, testComponentAddress, handler)
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// Create Component to connect to it
|
||||||
|
c := makeBasicComponent(defaultComponentName, testComponentAddress, t)
|
||||||
|
|
||||||
|
//========================================
|
||||||
|
// Connect the new Component to the server
|
||||||
|
err := c.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the Component is connected, let's set the xml.Decoder for the server
|
||||||
|
|
||||||
|
return c, mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component {
|
||||||
|
opts := ComponentOptions{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: mockServerAddr,
|
||||||
|
Domain: "localhost",
|
||||||
|
},
|
||||||
|
Domain: testComponentDomain,
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: name,
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
router := NewRouter()
|
||||||
|
c, err := NewComponent(opts, router, componentDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// This really should not be used as is.
|
||||||
|
// It's just meant to be a placeholder when error handling is not needed at this level
|
||||||
|
func componentDefaultErrorHandler(err error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends IQ response to Component request.
|
||||||
|
// No parsing of the request here. We just check that it's valid, and send the default response.
|
||||||
|
func handlerForComponentIQSend(t *testing.T, sc *ServerConn) {
|
||||||
|
// Completes the connection by exchanging handshakes
|
||||||
|
handlerForComponentHandshakeDefaultID(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for ID and handshake related tests
|
||||||
|
func checkOpenStreamHandshakeID(t *testing.T, sc *ServerConn, streamID string) {
|
||||||
|
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to set deadline: %v", err)
|
||||||
|
}
|
||||||
|
defer sc.connection.SetDeadline(time.Time{})
|
||||||
|
|
||||||
|
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
|
||||||
|
token, err := sc.decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read next token: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch elem := token.(type) {
|
||||||
|
// Wait for first startElement
|
||||||
|
case xml.StartElement:
|
||||||
|
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
|
||||||
|
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(sc.connection, serverStreamOpen, "localhost", streamID, stanza.NSComponent, stanza.NSStream); err != nil {
|
||||||
|
t.Errorf("cannot write server stream open: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOpenStreamHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||||
|
checkOpenStreamHandshakeID(t, sc, defaultStreamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant.
|
||||||
|
// This handler is supposed to fail by sending a "message" stanza instead of a <handshake/> stanza to finalize the handshake.
|
||||||
|
func handlerComponentFailedHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||||
|
checkOpenStreamHandshakeDefaultID(t, sc)
|
||||||
|
readHandshakeComponent(t, sc.decoder)
|
||||||
|
|
||||||
|
// Send a message, instead of a "<handshake/>" tag, to fail the handshake process dans disconnect the client.
|
||||||
|
me := stanza.Message{
|
||||||
|
Attrs: stanza.Attrs{Type: stanza.MessageTypeChat, From: defaultServerName, To: defaultComponentName, Lang: "en"},
|
||||||
|
Body: "Fail my handshake.",
|
||||||
|
}
|
||||||
|
s, _ := xml.Marshal(me)
|
||||||
|
_, err := sc.connection.Write(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not write message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads from the connection with the Component. Expects a handshake request, and returns the <handshake/> tag.
|
||||||
|
func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) {
|
||||||
|
se, err := stanza.NextStart(decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read auth: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nv := &stanza.Handshake{}
|
||||||
|
// Decode element into pointer storage
|
||||||
|
if err = decoder.DecodeElement(nv, &se); err != nil {
|
||||||
|
t.Errorf("cannot decode handshake: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(strings.TrimSpace(nv.Value)) == 0 {
|
||||||
|
t.Errorf("did not receive handshake ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant.
|
||||||
|
// Used in the mock server as a Handler
|
||||||
|
func handlerForComponentHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||||
|
checkOpenStreamHandshakeDefaultID(t, sc)
|
||||||
|
readHandshakeComponent(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<handshake/>")) // That's all the server needs to return (see xep-0114)
|
||||||
|
return
|
||||||
|
}
|
35
config.go
Normal file
35
config.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config & TransportConfiguration must not be modified after having been passed to NewClient. Any
|
||||||
|
// changes made after connecting are ignored.
|
||||||
|
type Config struct {
|
||||||
|
TransportConfiguration
|
||||||
|
|
||||||
|
Jid string
|
||||||
|
parsedJid *stanza.Jid // For easier manipulation
|
||||||
|
Credential Credential
|
||||||
|
StreamLogger *os.File // Used for debugging
|
||||||
|
Lang string // TODO: should default to 'en'
|
||||||
|
KeepaliveInterval time.Duration // Interval between keepalive packets
|
||||||
|
ConnectTimeout int // Client timeout in seconds. Default to 15
|
||||||
|
// Insecure can be set to true to allow to open a session without TLS. If TLS
|
||||||
|
// is supported on the server, we will still try to use it.
|
||||||
|
Insecure bool
|
||||||
|
|
||||||
|
// Activate stream management process during session
|
||||||
|
StreamManagementEnable bool
|
||||||
|
// Enable stream management resume capability
|
||||||
|
streamManagementResume bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStreamResumable tells if a stream session is resumable by reading the "config" part of a client.
|
||||||
|
// It checks if stream management is enabled, and if stream resumption was set and accepted by the server.
|
||||||
|
func IsStreamResumable(c *Client) bool {
|
||||||
|
return c.config.StreamManagementEnable && c.config.streamManagementResume
|
||||||
|
}
|
33
conn_error.go
Normal file
33
conn_error.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnError struct {
|
||||||
|
frame xerrors.Frame
|
||||||
|
err error
|
||||||
|
// Permanent will be true if error is not recoverable
|
||||||
|
Permanent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnError(err error, permanent bool) ConnError {
|
||||||
|
return ConnError{err: err, frame: xerrors.Caller(1), Permanent: permanent}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ConnError) Format(s fmt.State, verb rune) {
|
||||||
|
xerrors.FormatError(e, s, verb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ConnError) FormatError(p xerrors.Printer) error {
|
||||||
|
e.frame.Format(p)
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ConnError) Error() string {
|
||||||
|
return fmt.Sprint(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ConnError) Unwrap() error { return e.err }
|
42
doc.go
Normal file
42
doc.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
Fluux XMPP is an modern and full-featured XMPP library that can be used to build clients or
|
||||||
|
server components.
|
||||||
|
|
||||||
|
The goal is to make simple to write modern compliant XMPP software:
|
||||||
|
|
||||||
|
- For automation (like for example monitoring of an XMPP service),
|
||||||
|
- For building connected "things" by plugging them on an XMPP server,
|
||||||
|
- For writing simple chatbots to control a service or a thing.
|
||||||
|
- For writing XMPP servers components. Fluux XMPP supports:
|
||||||
|
- XEP-0114: Jabber Component Protocol
|
||||||
|
- XEP-0355: Namespace Delegation
|
||||||
|
- XEP-0356: Privileged Entity
|
||||||
|
|
||||||
|
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
|
||||||
|
|
||||||
|
The library includes a StreamManager that provides features like autoreconnect exponential back-off.
|
||||||
|
|
||||||
|
The library is implementing latest versions of the XMPP specifications (RFC 6120 and RFC 6121), and includes
|
||||||
|
support for many extensions.
|
||||||
|
|
||||||
|
Clients
|
||||||
|
|
||||||
|
Fluux XMPP can be use to create fully interactive XMPP clients (for
|
||||||
|
example console-based), but it is more commonly used to build automated
|
||||||
|
clients (connected devices, automation scripts, chatbots, etc.).
|
||||||
|
|
||||||
|
Components
|
||||||
|
|
||||||
|
XMPP components can typically be used to extends the features of an XMPP
|
||||||
|
server, in a portable way, using component protocol over persistent TCP
|
||||||
|
serverConnections.
|
||||||
|
|
||||||
|
Component protocol is defined in XEP-114 (https://xmpp.org/extensions/xep-0114.html).
|
||||||
|
|
||||||
|
Compliance
|
||||||
|
|
||||||
|
Fluux XMPP has been primarily tested with ejabberd (https://www.ejabberd.im)
|
||||||
|
but it should work with any XMPP compliant server.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package xmpp
|
10
go.mod
Normal file
10
go.mod
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module gosrc.io/xmpp
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/go-cmp v0.3.1
|
||||||
|
github.com/google/uuid v1.1.1
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||||
|
nhooyr.io/websocket v1.6.5
|
||||||
|
)
|
210
go.sum
Normal file
210
go.sum
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
|
||||||
|
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
32
network.go
Normal file
32
network.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ensurePort adds a port to an address if none are provided.
|
||||||
|
// It handles both IPV4 and IPV6 addresses.
|
||||||
|
func ensurePort(addr string, port int) string {
|
||||||
|
// This is an IPV6 address literal
|
||||||
|
if strings.HasPrefix(addr, "[") {
|
||||||
|
// if address has no port (behind his ipv6 address) - add default port
|
||||||
|
if strings.LastIndex(addr, ":") <= strings.LastIndex(addr, "]") {
|
||||||
|
return addr + ":" + strconv.Itoa(port)
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is either an IPV6 address without bracket or an IPV4 address
|
||||||
|
switch strings.Count(addr, ":") {
|
||||||
|
case 0:
|
||||||
|
// This is IPV4 without port
|
||||||
|
return addr + ":" + strconv.Itoa(port)
|
||||||
|
case 1:
|
||||||
|
// This is IPV6 with port
|
||||||
|
return addr
|
||||||
|
default:
|
||||||
|
// This is IPV6 without port, as you need to use bracket with port in IPV6
|
||||||
|
return "[" + addr + "]:" + strconv.Itoa(port)
|
||||||
|
}
|
||||||
|
}
|
66
network_test.go
Normal file
66
network_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseAddr(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "ipv4-no-port-1", input: "localhost", want: "localhost:5222"},
|
||||||
|
{name: "ipv4-with-port-1", input: "localhost:5555", want: "localhost:5555"},
|
||||||
|
{name: "ipv4-no-port-2", input: "127.0.0.1", want: "127.0.0.1:5222"},
|
||||||
|
{name: "ipv4-with-port-2", input: "127.0.0.1:5555", want: "127.0.0.1:5555"},
|
||||||
|
{name: "ipv6-no-port-1", input: "::1", want: "[::1]:5222"},
|
||||||
|
{name: "ipv6-no-port-2", input: "[::1]", want: "[::1]:5222"},
|
||||||
|
{name: "ipv6-no-port-3", input: "2001::7334", want: "[2001::7334]:5222"},
|
||||||
|
{name: "ipv6-no-port-4", input: "2001:db8:85a3:0:0:8a2e:370:7334", want: "[2001:db8:85a3:0:0:8a2e:370:7334]:5222"},
|
||||||
|
{name: "ipv6-with-port-1", input: "[::1]:5555", want: "[::1]:5555"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(st *testing.T) {
|
||||||
|
addr := ensurePort(tc.input, 5222)
|
||||||
|
|
||||||
|
if addr != tc.want {
|
||||||
|
st.Errorf("incorrect Result: %v (!= %v)", addr, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsurePort(t *testing.T) {
|
||||||
|
testAddresses := []string{
|
||||||
|
"1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad",
|
||||||
|
"1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad:5252",
|
||||||
|
"[::1]",
|
||||||
|
"127.0.0.1:5555",
|
||||||
|
"127.0.0.1",
|
||||||
|
"[::1]:5555",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, oldAddr := range testAddresses {
|
||||||
|
t.Run(oldAddr, func(st *testing.T) {
|
||||||
|
newAddr := ensurePort(oldAddr, 5222)
|
||||||
|
|
||||||
|
if len(newAddr) < len(oldAddr) {
|
||||||
|
st.Errorf("incorrect Result: transformed address is shorter than input : %v (old) > %v (new)", newAddr, oldAddr)
|
||||||
|
}
|
||||||
|
// If IPv6, the new address needs brackets to specify a port, like so : [2001:db8:85a3:0:0:8a2e:370:7334]:5222
|
||||||
|
if strings.Count(newAddr, "[") < strings.Count(oldAddr, "[") ||
|
||||||
|
strings.Count(newAddr, "]") < strings.Count(oldAddr, "]") {
|
||||||
|
|
||||||
|
st.Errorf("incorrect Result. Transformed address seems to not have correct brakets : %v => %v", oldAddr, newAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we messed up the colons, or didn't properly add a port
|
||||||
|
if strings.Count(newAddr, ":") < strings.Count(oldAddr, ":") {
|
||||||
|
st.Errorf("incorrect Result: transformed address doesn't seem to have a port %v (=> %v, no port ?)", oldAddr, newAddr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
374
router.go
Normal file
374
router.go
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
The XMPP router helps client and component developers select which XMPP they would like to process,
|
||||||
|
and associate processing code depending on the router configuration.
|
||||||
|
|
||||||
|
Here are important rules to keep in mind while setting your routes and matchers:
|
||||||
|
- Routes are evaluated in the order they are set.
|
||||||
|
- When a route matches, it is executed and all others routes are ignored. For each packet, only a single
|
||||||
|
route is executed.
|
||||||
|
- An empty route will match everything. Adding an empty route as the last route in your router will
|
||||||
|
allow you to get all stanzas that did not match any previous route. You can for example use this to
|
||||||
|
log all unexpected stanza received by your client or component.
|
||||||
|
|
||||||
|
TODO: Automatically reply to IQ that do not match any route, to comply to XMPP standard.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Router struct {
|
||||||
|
// Routes to be matched, in order.
|
||||||
|
routes []*Route
|
||||||
|
|
||||||
|
IQResultRoutes map[string]*IQResultRoute
|
||||||
|
IQResultRouteLock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter returns a new router instance.
|
||||||
|
func NewRouter() *Router {
|
||||||
|
return &Router{
|
||||||
|
IQResultRoutes: make(map[string]*IQResultRoute),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// route is called by the XMPP client to dispatch stanza received using the set up routes.
|
||||||
|
// It is also used by test, but is not supposed to be used directly by users of the library.
|
||||||
|
func (r *Router) route(s Sender, p stanza.Packet) {
|
||||||
|
a, isA := p.(stanza.SMAnswer)
|
||||||
|
if isA {
|
||||||
|
switch tt := s.(type) {
|
||||||
|
case *Client:
|
||||||
|
lastAcked := a.H
|
||||||
|
SendMissingStz(int(lastAcked), s, tt.Session.SMState.UnAckQueue)
|
||||||
|
case *Component:
|
||||||
|
// TODO
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iq, isIq := p.(*stanza.IQ)
|
||||||
|
if isIq {
|
||||||
|
r.IQResultRouteLock.RLock()
|
||||||
|
route, ok := r.IQResultRoutes[iq.Id]
|
||||||
|
r.IQResultRouteLock.RUnlock()
|
||||||
|
if ok {
|
||||||
|
r.IQResultRouteLock.Lock()
|
||||||
|
delete(r.IQResultRoutes, iq.Id)
|
||||||
|
r.IQResultRouteLock.Unlock()
|
||||||
|
route.result <- *iq
|
||||||
|
close(route.result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var match RouteMatch
|
||||||
|
if r.Match(p, &match) {
|
||||||
|
// If we match, route the packet
|
||||||
|
match.Handler.HandlePacket(s, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no match and we receive an iq set or get, we need to send a reply
|
||||||
|
if isIq && (iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet) {
|
||||||
|
iqNotImplemented(s, iq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMissingStz sends all stanzas that did not reach the server, according to the response to an ack request (see XEP-0198, acks)
|
||||||
|
func SendMissingStz(lastSent int, s Sender, uaq *stanza.UnAckQueue) error {
|
||||||
|
uaq.RWMutex.Lock()
|
||||||
|
if len(uaq.Uslice) <= 0 {
|
||||||
|
uaq.RWMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
last := uaq.Uslice[len(uaq.Uslice)-1]
|
||||||
|
if last.Id > lastSent {
|
||||||
|
// Remove sent stanzas from the queue
|
||||||
|
uaq.PopN(lastSent - last.Id)
|
||||||
|
// Re-send non acknowledged stanzas
|
||||||
|
for _, elt := range uaq.PopN(len(uaq.Uslice)) {
|
||||||
|
eltStz := elt.(*stanza.UnAckedStz)
|
||||||
|
err := s.SendRaw(eltStz.Stz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Ask for updates on stanzas we just sent to the entity. Not sure I should leave this. Maybe let users call ack again by themselves ?
|
||||||
|
s.Send(stanza.SMRequest{})
|
||||||
|
}
|
||||||
|
uaq.RWMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func iqNotImplemented(s Sender, iq *stanza.IQ) {
|
||||||
|
err := stanza.Err{
|
||||||
|
XMLName: xml.Name{Local: "error"},
|
||||||
|
Code: 501,
|
||||||
|
Type: "cancel",
|
||||||
|
Reason: "feature-not-implemented",
|
||||||
|
}
|
||||||
|
reply := iq.MakeError(err)
|
||||||
|
_ = s.Send(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoute registers an empty routes
|
||||||
|
func (r *Router) NewRoute() *Route {
|
||||||
|
route := &Route{}
|
||||||
|
r.routes = append(r.routes, route)
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIQResultRoute register a route that will catch an IQ result stanza with
|
||||||
|
// the given Id. The route will only match ones, after which it will automatically
|
||||||
|
// be unregistered
|
||||||
|
func (r *Router) NewIQResultRoute(ctx context.Context, id string) chan stanza.IQ {
|
||||||
|
route := NewIQResultRoute(ctx)
|
||||||
|
r.IQResultRouteLock.Lock()
|
||||||
|
r.IQResultRoutes[id] = route
|
||||||
|
r.IQResultRouteLock.Unlock()
|
||||||
|
|
||||||
|
// Start a go function to make sure the route is unregistered when the context
|
||||||
|
// is done.
|
||||||
|
go func() {
|
||||||
|
<-route.context.Done()
|
||||||
|
r.IQResultRouteLock.Lock()
|
||||||
|
delete(r.IQResultRoutes, id)
|
||||||
|
r.IQResultRouteLock.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return route.result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
|
for _, route := range r.routes {
|
||||||
|
if route.Match(p, match) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle registers a new route with a matcher for a given packet name (iq, message, presence)
|
||||||
|
// See Route.Packet() and Route.Handler().
|
||||||
|
func (r *Router) Handle(name string, handler Handler) *Route {
|
||||||
|
return r.NewRoute().Packet(name).Handler(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence)
|
||||||
|
// See Route.Path() and Route.HandlerFunc().
|
||||||
|
func (r *Router) HandleFunc(name string, f func(s Sender, p stanza.Packet)) *Route {
|
||||||
|
return r.NewRoute().Packet(name).HandlerFunc(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// TimeoutHandlerFunc is a function type for handling IQ result timeouts.
|
||||||
|
type TimeoutHandlerFunc func(err error)
|
||||||
|
|
||||||
|
// IQResultRoute is a temporary route to match IQ result stanzas
|
||||||
|
type IQResultRoute struct {
|
||||||
|
context context.Context
|
||||||
|
result chan stanza.IQ
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIQResultRoute creates a new IQResultRoute instance
|
||||||
|
func NewIQResultRoute(ctx context.Context) *IQResultRoute {
|
||||||
|
return &IQResultRoute{
|
||||||
|
context: ctx,
|
||||||
|
result: make(chan stanza.IQ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// IQ result handler
|
||||||
|
|
||||||
|
// IQResultHandler is a utility interface for IQ result handlers
|
||||||
|
type IQResultHandler interface {
|
||||||
|
HandleIQ(ctx context.Context, s Sender, iq stanza.IQ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IQResultHandlerFunc is an adapter to allow using functions as IQ result handlers.
|
||||||
|
type IQResultHandlerFunc func(ctx context.Context, s Sender, iq stanza.IQ)
|
||||||
|
|
||||||
|
// HandleIQ is a proxy function to implement IQResultHandler using a function.
|
||||||
|
func (f IQResultHandlerFunc) HandleIQ(ctx context.Context, s Sender, iq stanza.IQ) {
|
||||||
|
f(ctx, s, iq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
HandlePacket(s Sender, p stanza.Packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
handler Handler
|
||||||
|
// Matchers are used to "specialize" routes and focus on specific packet features
|
||||||
|
matchers []Matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) Handler(handler Handler) *Route {
|
||||||
|
r.handler = handler
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HandlerFunc type is an adapter to allow the use of
|
||||||
|
// ordinary functions as XMPP handlers. If f is a function
|
||||||
|
// with the appropriate signature, HandlerFunc(f) is a
|
||||||
|
// Handler that calls f.
|
||||||
|
type HandlerFunc func(s Sender, p stanza.Packet)
|
||||||
|
|
||||||
|
// HandlePacket calls f(s, p)
|
||||||
|
func (f HandlerFunc) HandlePacket(s Sender, p stanza.Packet) {
|
||||||
|
f(s, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerFunc sets a handler function for the route
|
||||||
|
func (r *Route) HandlerFunc(f HandlerFunc) *Route {
|
||||||
|
return r.Handler(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMatcher adds a matcher to the route
|
||||||
|
func (r *Route) AddMatcher(m Matcher) *Route {
|
||||||
|
r.matchers = append(r.matchers, m)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
|
for _, m := range r.matchers {
|
||||||
|
if matched := m.Match(p, match); !matched {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a match, let's pass info route match info
|
||||||
|
match.Route = r
|
||||||
|
match.Handler = r.handler
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// Match on packet name
|
||||||
|
|
||||||
|
type nameMatcher string
|
||||||
|
|
||||||
|
func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
|
var name string
|
||||||
|
// TODO: To avoid type switch everywhere in matching, I think we will need to have
|
||||||
|
// to move to a concrete type for packets, to make matching and comparison more natural.
|
||||||
|
// Current code structure is probably too rigid.
|
||||||
|
// Maybe packet types should even be from an enum.
|
||||||
|
switch p.(type) {
|
||||||
|
case stanza.Message:
|
||||||
|
name = "message"
|
||||||
|
case *stanza.IQ:
|
||||||
|
name = "iq"
|
||||||
|
case stanza.Presence:
|
||||||
|
name = "presence"
|
||||||
|
}
|
||||||
|
if name == string(n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packet matches on a packet name (iq, message, presence, ...)
|
||||||
|
// It matches on the Local part of the xml.Name
|
||||||
|
func (r *Route) Packet(name string) *Route {
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
return r.AddMatcher(nameMatcher(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Match on stanza type
|
||||||
|
|
||||||
|
// nsTypeMather matches on a list of IQ payload namespaces
|
||||||
|
type nsTypeMatcher []string
|
||||||
|
|
||||||
|
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
|
var stanzaType stanza.StanzaType
|
||||||
|
switch packet := p.(type) {
|
||||||
|
case *stanza.IQ:
|
||||||
|
stanzaType = packet.Type
|
||||||
|
case stanza.Presence:
|
||||||
|
stanzaType = packet.Type
|
||||||
|
case stanza.Message:
|
||||||
|
if packet.Type == "" {
|
||||||
|
// optional on message, normal is the default type
|
||||||
|
stanzaType = "normal"
|
||||||
|
} else {
|
||||||
|
stanzaType = packet.Type
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return matchInArray(m, string(stanzaType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IQNamespaces adds an IQ matcher, expecting both an IQ and a
|
||||||
|
func (r *Route) StanzaType(types ...string) *Route {
|
||||||
|
for k, v := range types {
|
||||||
|
types[k] = strings.ToLower(v)
|
||||||
|
}
|
||||||
|
return r.AddMatcher(nsTypeMatcher(types))
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Match on IQ and namespace
|
||||||
|
|
||||||
|
// nsIqMather matches on a list of IQ payload namespaces
|
||||||
|
type nsIQMatcher []string
|
||||||
|
|
||||||
|
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if iq.Payload == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return matchInArray(m, iq.Payload.Namespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IQNamespaces adds an IQ matcher, expecting both an IQ and a
|
||||||
|
func (r *Route) IQNamespaces(namespaces ...string) *Route {
|
||||||
|
for k, v := range namespaces {
|
||||||
|
namespaces[k] = strings.ToLower(v)
|
||||||
|
}
|
||||||
|
return r.AddMatcher(nsIQMatcher(namespaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Matchers
|
||||||
|
|
||||||
|
// Matchers are used to "specialize" routes and focus on specific packet features.
|
||||||
|
// You can register attach them to a route via the AddMatcher method.
|
||||||
|
type Matcher interface {
|
||||||
|
Match(stanza.Packet, *RouteMatch) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteMatch extracts and gather match information
|
||||||
|
type RouteMatch struct {
|
||||||
|
Route *Route
|
||||||
|
Handler Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchInArray is a generic matching function to check if a string is a list
|
||||||
|
// of specific function
|
||||||
|
func matchInArray(arr []string, value string) bool {
|
||||||
|
for _, str := range arr {
|
||||||
|
if str == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
332
router_test.go
Normal file
332
router_test.go
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test route & matchers
|
||||||
|
|
||||||
|
func TestIQResultRoutes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
router := NewRouter()
|
||||||
|
conn := NewSenderMock()
|
||||||
|
|
||||||
|
if router.IQResultRoutes == nil {
|
||||||
|
t.Fatal("NewRouter does not initialize isResultRoutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the IQ handler was called
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||||
|
defer cancel()
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, Id: "1234"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
res := router.NewIQResultRoute(ctx, "1234")
|
||||||
|
go router.route(conn, iq)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("IQ result was not matched")
|
||||||
|
case <-res:
|
||||||
|
// Success
|
||||||
|
}
|
||||||
|
|
||||||
|
// The match must only happen once, so the id should no longer be in IQResultRoutes
|
||||||
|
if _, ok := router.IQResultRoutes[iq.Attrs.Id]; ok {
|
||||||
|
t.Fatal("IQ ID was not removed from the route map")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check other IQ does not matcah
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||||
|
defer cancel()
|
||||||
|
iq.Attrs.Id = "4321"
|
||||||
|
res = router.NewIQResultRoute(ctx, "1234")
|
||||||
|
go router.route(conn, iq)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Success
|
||||||
|
case <-res:
|
||||||
|
t.Fatal("IQ result with wrong ID was matched")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameMatcher(t *testing.T) {
|
||||||
|
router := NewRouter()
|
||||||
|
router.HandleFunc("message", func(s Sender, p stanza.Packet) {
|
||||||
|
_ = s.SendRaw(successFlag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check that a message packet is properly matched
|
||||||
|
conn := NewSenderMock()
|
||||||
|
msg := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, To: "test@localhost", Id: "1"})
|
||||||
|
msg.Body = "Hello"
|
||||||
|
router.route(conn, msg)
|
||||||
|
if conn.String() != successFlag {
|
||||||
|
t.Error("Message was not matched and routed properly")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that an IQ packet is not matched
|
||||||
|
conn = NewSenderMock()
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
iq.Payload = &stanza.DiscoInfo{}
|
||||||
|
router.route(conn, iq)
|
||||||
|
if conn.String() == successFlag {
|
||||||
|
t.Error("IQ should not have been matched and routed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIQNSMatcher(t *testing.T) {
|
||||||
|
router := NewRouter()
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces(stanza.NSDiscoInfo, stanza.NSDiscoItems).
|
||||||
|
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||||
|
_ = s.SendRaw(successFlag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check that an IQ with proper namespace does match
|
||||||
|
conn := NewSenderMock()
|
||||||
|
iqDisco, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create iqDisco: %v", err)
|
||||||
|
}
|
||||||
|
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||||
|
iqDisco.Payload = &stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: stanza.NSDiscoInfo,
|
||||||
|
Local: "query",
|
||||||
|
}}
|
||||||
|
router.route(conn, iqDisco)
|
||||||
|
if conn.String() != successFlag {
|
||||||
|
t.Errorf("IQ should have been matched and routed: %v", iqDisco)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that another namespace is not matched
|
||||||
|
conn = NewSenderMock()
|
||||||
|
iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create iqVersion: %v", err)
|
||||||
|
}
|
||||||
|
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||||
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "jabber:iq:version",
|
||||||
|
Local: "query",
|
||||||
|
}}
|
||||||
|
router.route(conn, iqVersion)
|
||||||
|
if conn.String() == successFlag {
|
||||||
|
t.Errorf("IQ should not have been matched and routed: %v", iqVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeMatcher(t *testing.T) {
|
||||||
|
router := NewRouter()
|
||||||
|
router.NewRoute().
|
||||||
|
StanzaType("normal").
|
||||||
|
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||||
|
_ = s.SendRaw(successFlag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check that a packet with the proper type matches
|
||||||
|
conn := NewSenderMock()
|
||||||
|
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
|
||||||
|
message.Body = "hello"
|
||||||
|
router.route(conn, message)
|
||||||
|
|
||||||
|
if conn.String() != successFlag {
|
||||||
|
t.Errorf("'normal' message should have been matched and routed: %v", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should match on default type 'normal' for message without a type
|
||||||
|
conn = NewSenderMock()
|
||||||
|
message = stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
|
||||||
|
message.Body = "hello"
|
||||||
|
router.route(conn, message)
|
||||||
|
|
||||||
|
if conn.String() != successFlag {
|
||||||
|
t.Errorf("message should have been matched and routed: %v", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not match on other types
|
||||||
|
conn = NewSenderMock()
|
||||||
|
iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create iqVersion: %v", err)
|
||||||
|
}
|
||||||
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "jabber:iq:version",
|
||||||
|
Local: "query",
|
||||||
|
}}
|
||||||
|
router.route(conn, iqVersion)
|
||||||
|
|
||||||
|
if conn.String() == successFlag {
|
||||||
|
t.Errorf("iq get should not have been matched and routed: %v", iqVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompositeMatcher(t *testing.T) {
|
||||||
|
router := NewRouter()
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces("jabber:iq:version").
|
||||||
|
StanzaType(string(stanza.IQTypeGet)).
|
||||||
|
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||||
|
_ = s.SendRaw(successFlag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Data set
|
||||||
|
getVersionIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create getVersionIq: %v", err)
|
||||||
|
}
|
||||||
|
getVersionIq.Payload = &stanza.Version{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "jabber:iq:version",
|
||||||
|
Local: "query",
|
||||||
|
}}
|
||||||
|
|
||||||
|
setVersionIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create setVersionIq: %v", err)
|
||||||
|
}
|
||||||
|
setVersionIq.Payload = &stanza.Version{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "jabber:iq:version",
|
||||||
|
Local: "query",
|
||||||
|
}}
|
||||||
|
|
||||||
|
getDiscoIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create getDiscoIq: %v", err)
|
||||||
|
}
|
||||||
|
getDiscoIq.Payload = &stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://jabber.org/protocol/disco#info",
|
||||||
|
Local: "query",
|
||||||
|
}}
|
||||||
|
|
||||||
|
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
|
||||||
|
message.Body = "hello"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input stanza.Packet
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "match get version iq", input: getVersionIq, want: true},
|
||||||
|
{name: "ignore set version iq", input: setVersionIq, want: false},
|
||||||
|
{name: "ignore get discoinfo iq", input: getDiscoIq, want: false},
|
||||||
|
{name: "ignore message", input: message, want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(st *testing.T) {
|
||||||
|
conn := NewSenderMock()
|
||||||
|
router.route(conn, tc.input)
|
||||||
|
|
||||||
|
res := conn.String() == successFlag
|
||||||
|
if tc.want != res {
|
||||||
|
st.Errorf("incorrect result for %#v\nMatch = %#v, expecting %#v", tc.input, res, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A blank route with empty matcher will always match
|
||||||
|
// It can be use to receive all packets that do not match any of the previous route.
|
||||||
|
func TestCatchallMatcher(t *testing.T) {
|
||||||
|
router := NewRouter()
|
||||||
|
router.NewRoute().
|
||||||
|
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||||
|
_ = s.SendRaw(successFlag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check that we match on several packets
|
||||||
|
conn := NewSenderMock()
|
||||||
|
message := stanza.NewMessage(stanza.Attrs{Type: "chat", To: "test@localhost", Id: "1"})
|
||||||
|
message.Body = "hello"
|
||||||
|
router.route(conn, message)
|
||||||
|
|
||||||
|
if conn.String() != successFlag {
|
||||||
|
t.Errorf("chat message should have been matched and routed: %v", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = NewSenderMock()
|
||||||
|
iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create iqVersion: %v", err)
|
||||||
|
}
|
||||||
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "jabber:iq:version",
|
||||||
|
Local: "query",
|
||||||
|
}}
|
||||||
|
router.route(conn, iqVersion)
|
||||||
|
|
||||||
|
if conn.String() != successFlag {
|
||||||
|
t.Errorf("iq get should have been matched and routed: %v", iqVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SenderMock
|
||||||
|
|
||||||
|
const successFlag = "matched"
|
||||||
|
const cancelledFlag = "cancelled"
|
||||||
|
|
||||||
|
type SenderMock struct {
|
||||||
|
buffer *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSenderMock() SenderMock {
|
||||||
|
return SenderMock{buffer: new(bytes.Buffer)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SenderMock) Send(packet stanza.Packet) error {
|
||||||
|
out, err := xml.Marshal(packet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.buffer.Write(out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SenderMock) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
|
||||||
|
out, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.buffer.Write(out)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SenderMock) SendRaw(str string) error {
|
||||||
|
s.buffer.WriteString(str)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SenderMock) String() string {
|
||||||
|
return s.buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSenderMock(t *testing.T) {
|
||||||
|
conn := NewSenderMock()
|
||||||
|
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
|
||||||
|
msg.Body = "Hello"
|
||||||
|
if err := conn.Send(msg); err != nil {
|
||||||
|
t.Error("Could not send message")
|
||||||
|
}
|
||||||
|
if conn.String() != "<message id=\"1\" to=\"test@localhost\"><body>Hello</body></message>" {
|
||||||
|
t.Errorf("Incorrect packet sent: %s", conn.String())
|
||||||
|
}
|
||||||
|
}
|
342
session.go
Normal file
342
session.go
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
// Session info
|
||||||
|
BindJid string // Jabber ID as provided by XMPP server
|
||||||
|
StreamId string
|
||||||
|
SMState SMState
|
||||||
|
Features stanza.StreamFeatures
|
||||||
|
TlsEnabled bool
|
||||||
|
lastPacketId int
|
||||||
|
|
||||||
|
// read / write
|
||||||
|
transport Transport
|
||||||
|
|
||||||
|
// error management
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(c *Client, state SMState) (*Session, error) {
|
||||||
|
var s *Session
|
||||||
|
if c.Session == nil {
|
||||||
|
s = new(Session)
|
||||||
|
s.transport = c.transport
|
||||||
|
s.SMState = state
|
||||||
|
s.init()
|
||||||
|
} else {
|
||||||
|
s = c.Session
|
||||||
|
// We keep information about the previously set session, like the session ID, but we read server provided
|
||||||
|
// info again in case it changed between session break and resume, such as features.
|
||||||
|
s.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.err != nil {
|
||||||
|
return nil, NewConnError(s.err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.transport.IsSecure() {
|
||||||
|
s.startTlsIfSupported(c.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.transport.IsSecure() && !c.config.Insecure {
|
||||||
|
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
||||||
|
return nil, NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TlsEnabled {
|
||||||
|
s.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth
|
||||||
|
s.auth(c.config)
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
s.reset()
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt resumption
|
||||||
|
if s.resume(c.config) {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, bind resource and 'start' XMPP session
|
||||||
|
s.bind(c.config)
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
s.rfc3921Session()
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable stream management if supported
|
||||||
|
s.EnableStreamManagement(c.config)
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) PacketId() string {
|
||||||
|
s.lastPacketId++
|
||||||
|
return fmt.Sprintf("%x", s.lastPacketId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init gathers information on the session such as stream features from the server.
|
||||||
|
func (s *Session) init() {
|
||||||
|
s.Features = s.extractStreamFeatures()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) reset() {
|
||||||
|
if s.StreamId, s.err = s.transport.StartStream(); s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Features = s.extractStreamFeatures()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) extractStreamFeatures() (f stanza.StreamFeatures) {
|
||||||
|
// extract stream features
|
||||||
|
if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil {
|
||||||
|
s.err = errors.New("stream open decode features: " + s.err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) startTlsIfSupported(o *Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.transport.DoesStartTLS() {
|
||||||
|
if !o.Insecure {
|
||||||
|
s.err = errors.New("transport does not support starttls")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := s.Features.DoesStartTLS(); ok {
|
||||||
|
fmt.Fprintf(s.transport, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
|
||||||
|
var k stanza.TLSProceed
|
||||||
|
if s.err = s.transport.GetDecoder().DecodeElement(&k, nil); s.err != nil {
|
||||||
|
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.err = s.transport.StartTLS()
|
||||||
|
|
||||||
|
if s.err == nil {
|
||||||
|
s.TlsEnabled = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we do not allow cleartext serverConnections, make it explicit that server do not support starttls
|
||||||
|
if !o.Insecure {
|
||||||
|
s.err = errors.New("XMPP server does not advertise support for starttls")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) auth(o *Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.err = authSASL(s.transport, s.transport.GetDecoder(), s.Features, o.parsedJid.Node, o.Credential)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to resume session using stream management
|
||||||
|
func (s *Session) resume(o *Config) bool {
|
||||||
|
if !s.Features.DoesStreamManagement() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.SMState.Id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rsm := stanza.SMResume{
|
||||||
|
PrevId: s.SMState.Id,
|
||||||
|
H: &s.SMState.Inbound,
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(rsm)
|
||||||
|
|
||||||
|
_, err = s.transport.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var packet stanza.Packet
|
||||||
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||||
|
if s.err == nil {
|
||||||
|
switch p := packet.(type) {
|
||||||
|
case stanza.SMResumed:
|
||||||
|
if p.PrevId != s.SMState.Id {
|
||||||
|
s.err = errors.New("session resumption: mismatched id")
|
||||||
|
s.SMState = SMState{}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case stanza.SMFailed:
|
||||||
|
default:
|
||||||
|
s.err = errors.New("unexpected reply to SM resume")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.SMState = SMState{}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) bind(o *Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send IQ message asking to bind to the local user name.
|
||||||
|
var resource = o.parsedJid.Resource
|
||||||
|
iqB, err := stanza.NewIQ(stanza.Attrs{
|
||||||
|
Type: stanza.IQTypeSet,
|
||||||
|
Id: s.PacketId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have a resource name, and include it in the request if so
|
||||||
|
if resource != "" {
|
||||||
|
iqB.Payload = &stanza.Bind{
|
||||||
|
Resource: resource,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iqB.Payload = &stanza.Bind{}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the bind request IQ
|
||||||
|
data, err := xml.Marshal(iqB)
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := s.transport.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
} else if n == 0 {
|
||||||
|
s.err = errors.New("failed to write bind iq stanza to the server : wrote 0 bytes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the server response
|
||||||
|
var iq stanza.IQ
|
||||||
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||||
|
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Check all elements
|
||||||
|
switch payload := iq.Payload.(type) {
|
||||||
|
case *stanza.Bind:
|
||||||
|
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
|
||||||
|
default:
|
||||||
|
s.err = errors.New("iq bind result missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the bind, if the session is not optional (as per old RFC 3921), we send the session open iq.
|
||||||
|
func (s *Session) rfc3921Session() {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var iq stanza.IQ
|
||||||
|
// We only negotiate session binding if it is mandatory, we skip it when optional.
|
||||||
|
if !s.Features.Session.IsOptional() {
|
||||||
|
se, err := stanza.NewIQ(stanza.Attrs{
|
||||||
|
Type: stanza.IQTypeSet,
|
||||||
|
Id: s.PacketId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
se.Payload = &stanza.StreamSession{}
|
||||||
|
data, err := xml.Marshal(se)
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := s.transport.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
} else if n == 0 {
|
||||||
|
s.err = errors.New("there was a problem marshaling the session IQ : wrote 0 bytes to server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||||
|
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable stream management, with session resumption, if supported.
|
||||||
|
func (s *Session) EnableStreamManagement(o *Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.Features.DoesStreamManagement() || !o.StreamManagementEnable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := stanza.NewUnAckQueue()
|
||||||
|
ebleNonza := stanza.SMEnable{Resume: &o.streamManagementResume}
|
||||||
|
pktStr, err := xml.Marshal(ebleNonza)
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = s.transport.Write(pktStr)
|
||||||
|
if err != nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var packet stanza.Packet
|
||||||
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||||
|
if s.err == nil {
|
||||||
|
switch p := packet.(type) {
|
||||||
|
case stanza.SMEnabled:
|
||||||
|
// Server allows resumption or not using SMEnabled attribute "resume". We must read the server response
|
||||||
|
// and update config accordingly
|
||||||
|
b, err := strconv.ParseBool(p.Resume)
|
||||||
|
if err != nil || !b {
|
||||||
|
o.StreamManagementEnable = false
|
||||||
|
}
|
||||||
|
s.SMState = SMState{Id: p.Id, preferredReconAddr: p.Location}
|
||||||
|
s.SMState.UnAckQueue = q
|
||||||
|
case stanza.SMFailed:
|
||||||
|
// TODO: Store error in SMState, for later inspection
|
||||||
|
s.SMState = SMState{StreamErrorGroup: p.StreamErrorGroup}
|
||||||
|
s.SMState.UnAckQueue = q
|
||||||
|
s.err = errors.New("failed to establish session : " + s.SMState.StreamErrorGroup.GroupErrorName())
|
||||||
|
default:
|
||||||
|
s.err = errors.New("unexpected reply to SM enable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
142
stanza/README.md
Normal file
142
stanza/README.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# XMPP Stanza
|
||||||
|
|
||||||
|
XMPP `stanza` package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
||||||
|
|
||||||
|
## Stanza creation
|
||||||
|
|
||||||
|
When creating stanzas, you can use two approaches:
|
||||||
|
|
||||||
|
1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the
|
||||||
|
stanza.
|
||||||
|
2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the
|
||||||
|
final stanza.
|
||||||
|
|
||||||
|
The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of
|
||||||
|
struct that you can build by hand.
|
||||||
|
|
||||||
|
### Composing stanzas manually with structs
|
||||||
|
|
||||||
|
Here is for example how you would generate an IQ discovery result:
|
||||||
|
|
||||||
|
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||||
|
identity := stanza.Identity{
|
||||||
|
Name: opts.Name,
|
||||||
|
Category: opts.Category,
|
||||||
|
Type: opts.Type,
|
||||||
|
}
|
||||||
|
payload := stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: stanza.NSDiscoInfo,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
Identity: []stanza.Identity{identity},
|
||||||
|
Features: []stanza.Feature{
|
||||||
|
{Var: stanza.NSDiscoInfo},
|
||||||
|
{Var: stanza.NSDiscoItems},
|
||||||
|
{Var: "jabber:iq:version"},
|
||||||
|
{Var: "urn:xmpp:delegation:1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqResp.Payload = &payload
|
||||||
|
|
||||||
|
### Using helpers
|
||||||
|
|
||||||
|
Here is for example how you would generate an IQ discovery result using Builder:
|
||||||
|
|
||||||
|
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||||
|
disco := iq.DiscoInfo()
|
||||||
|
disco.AddIdentity("Test Component", "gateway", "service")
|
||||||
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
|
||||||
|
## Payload and extensions
|
||||||
|
|
||||||
|
### Message
|
||||||
|
|
||||||
|
Here is the list of implemented message extensions:
|
||||||
|
|
||||||
|
- `Delegation`
|
||||||
|
|
||||||
|
- `Markable`
|
||||||
|
- `MarkAcknowledged`
|
||||||
|
- `MarkDisplayed`
|
||||||
|
- `MarkReceived`
|
||||||
|
|
||||||
|
- `StateActive`
|
||||||
|
- `StateComposing`
|
||||||
|
- `StateGone`
|
||||||
|
- `StateInactive`
|
||||||
|
- `StatePaused`
|
||||||
|
|
||||||
|
- `HTML`
|
||||||
|
|
||||||
|
- `OOB`
|
||||||
|
|
||||||
|
- `ReceiptReceived`
|
||||||
|
- `ReceiptRequest`
|
||||||
|
|
||||||
|
- `Mood`
|
||||||
|
|
||||||
|
### Presence
|
||||||
|
|
||||||
|
Here is the list of implemented presence extensions:
|
||||||
|
|
||||||
|
- `MucPresence`
|
||||||
|
|
||||||
|
### IQ
|
||||||
|
|
||||||
|
IQ (Information Queries) contain a payload associated with the request and possibly an error. The main difference with
|
||||||
|
Message and Presence extension is that you can only have one payload per IQ. The XMPP specification does not support
|
||||||
|
having multiple payloads.
|
||||||
|
|
||||||
|
Here is the list of structs implementing IQPayloads:
|
||||||
|
|
||||||
|
- `ControlSet`
|
||||||
|
- `ControlSetResponse`
|
||||||
|
- `Delegation`
|
||||||
|
- `DiscoInfo`
|
||||||
|
- `DiscoItems`
|
||||||
|
- `Pubsub`
|
||||||
|
- `Version`
|
||||||
|
- `Node`
|
||||||
|
|
||||||
|
Finally, when the payload of the parsed stanza is unknown, the parser will provide the unknown payload as a generic
|
||||||
|
`Node` element. You can also use the Node struct to add custom information on stanza generation. However, in both cases,
|
||||||
|
you may also consider [adding your own custom extensions on stanzas]().
|
||||||
|
|
||||||
|
|
||||||
|
## Adding your own custom extensions on stanzas
|
||||||
|
|
||||||
|
Extensions are registered on launch using the `Registry`. It can be used to register you own custom payload. You may
|
||||||
|
want to do so to support extensions we did not yet implement, or to add your own custom extensions to your XMPP stanzas.
|
||||||
|
|
||||||
|
To create an extension you need:
|
||||||
|
1. to create a struct for that extension. It need to have XMLName for consistency and to tagged at the struct level with
|
||||||
|
`xml` info.
|
||||||
|
2. It need to implement one or several extensions interface: stanza.IQPayload, stanza.MsgExtension and / or
|
||||||
|
stanza.PresExtension
|
||||||
|
3. Add that custom extension to the stanza.TypeRegistry during the file init.
|
||||||
|
|
||||||
|
Here an example code showing how to create a custom IQPayload.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package myclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomPayload struct {
|
||||||
|
XMLName xml.Name `xml:"my:custom:payload query"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomPayload) Namespace() string {
|
||||||
|
return c.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
|
||||||
|
}
|
||||||
|
```
|
157
stanza/commands.go
Normal file
157
stanza/commands.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// Implements the XEP-0050 extension
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommandActionCancel = "cancel"
|
||||||
|
CommandActionComplete = "complete"
|
||||||
|
CommandActionExecute = "execute"
|
||||||
|
CommandActionNext = "next"
|
||||||
|
CommandActionPrevious = "prev"
|
||||||
|
|
||||||
|
CommandStatusCancelled = "canceled"
|
||||||
|
CommandStatusCompleted = "completed"
|
||||||
|
CommandStatusExecuting = "executing"
|
||||||
|
|
||||||
|
CommandNoteTypeErr = "error"
|
||||||
|
CommandNoteTypeInfo = "info"
|
||||||
|
CommandNoteTypeWarn = "warn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"`
|
||||||
|
|
||||||
|
CommandElements []CommandElement
|
||||||
|
|
||||||
|
BadAction *struct{} `xml:"bad-action,omitempty"`
|
||||||
|
BadLocale *struct{} `xml:"bad-locale,omitempty"`
|
||||||
|
BadPayload *struct{} `xml:"bad-payload,omitempty"`
|
||||||
|
BadSessionId *struct{} `xml:"bad-sessionid,omitempty"`
|
||||||
|
MalformedAction *struct{} `xml:"malformed-action,omitempty"`
|
||||||
|
SessionExpired *struct{} `xml:"session-expired,omitempty"`
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
Action string `xml:"action,attr,omitempty"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
SessionId string `xml:"sessionid,attr,omitempty"`
|
||||||
|
Status string `xml:"status,attr,omitempty"`
|
||||||
|
Lang string `xml:"lang,attr,omitempty"`
|
||||||
|
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Namespace() string {
|
||||||
|
return c.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) GetSet() *ResultSet {
|
||||||
|
return c.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandElement interface {
|
||||||
|
Ref() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Actions struct {
|
||||||
|
XMLName xml.Name `xml:"actions"`
|
||||||
|
|
||||||
|
Prev *struct{} `xml:"prev,omitempty"`
|
||||||
|
Next *struct{} `xml:"next,omitempty"`
|
||||||
|
Complete *struct{} `xml:"complete,omitempty"`
|
||||||
|
|
||||||
|
Execute string `xml:"execute,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Actions) Ref() string {
|
||||||
|
return "actions"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Note struct {
|
||||||
|
XMLName xml.Name `xml:"note"`
|
||||||
|
|
||||||
|
Text string `xml:",cdata"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Note) Ref() string {
|
||||||
|
return "note"
|
||||||
|
}
|
||||||
|
func (f *Form) Ref() string { return "form" }
|
||||||
|
|
||||||
|
func (n *Node) Ref() string {
|
||||||
|
return "node"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
c.XMLName = start.Name
|
||||||
|
|
||||||
|
// Extract packet attributes
|
||||||
|
for _, attr := range start.Attr {
|
||||||
|
if attr.Name.Local == "action" {
|
||||||
|
c.Action = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "node" {
|
||||||
|
c.Node = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "sessionid" {
|
||||||
|
c.SessionId = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "status" {
|
||||||
|
c.Status = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "lang" {
|
||||||
|
c.Lang = attr.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode inner elements
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tt := t.(type) {
|
||||||
|
|
||||||
|
case xml.StartElement:
|
||||||
|
// Decode sub-elements
|
||||||
|
var err error
|
||||||
|
switch tt.Name.Local {
|
||||||
|
|
||||||
|
case "actions":
|
||||||
|
a := Actions{}
|
||||||
|
err = d.DecodeElement(&a, &tt)
|
||||||
|
c.CommandElements = append(c.CommandElements, &a)
|
||||||
|
case "note":
|
||||||
|
nt := Note{}
|
||||||
|
err = d.DecodeElement(&nt, &tt)
|
||||||
|
c.CommandElements = append(c.CommandElements, &nt)
|
||||||
|
case "x":
|
||||||
|
f := Form{}
|
||||||
|
err = d.DecodeElement(&f, &tt)
|
||||||
|
c.CommandElements = append(c.CommandElements, &f)
|
||||||
|
default:
|
||||||
|
n := Node{}
|
||||||
|
err = d.DecodeElement(&n, &tt)
|
||||||
|
c.CommandElements = append(c.CommandElements, &n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/commands", Local: "command"}, Command{})
|
||||||
|
}
|
40
stanza/commands_test.go
Normal file
40
stanza/commands_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalCommands(t *testing.T) {
|
||||||
|
input := "<command xmlns=\"http://jabber.org/protocol/commands\" node=\"list\" " +
|
||||||
|
"sessionid=\"list:20020923T213616Z-700\" status=\"completed\"><x xmlns=\"jabber:x:data\" " +
|
||||||
|
"type=\"result\"><title>Available Services</title><reported xmlns=\"jabber:x:data\"><field var=\"service\" " +
|
||||||
|
"label=\"Service\"></field><field var=\"runlevel-1\" label=\"Single-User mode\">" +
|
||||||
|
"</field><field var=\"runlevel-2\" label=\"Non-Networked Multi-User mode\"></field><field var=\"runlevel-3\" " +
|
||||||
|
"label=\"Full Multi-User mode\"></field><field var=\"runlevel-5\" label=\"X-Window mode\"></field></reported>" +
|
||||||
|
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>httpd</value></field><field var=\"runlevel-1\">" +
|
||||||
|
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
|
||||||
|
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
|
||||||
|
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>postgresql</value></field>" +
|
||||||
|
"<field var=\"runlevel-1\"><value>off</value></field><field var=\"runlevel-2\"><value>off</value></field>" +
|
||||||
|
"<field var=\"runlevel-3\"><value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
|
||||||
|
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>jabberd</value></field><field var=\"runlevel-1\">" +
|
||||||
|
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
|
||||||
|
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item></x></command>"
|
||||||
|
var c stanza.Command
|
||||||
|
err := xml.Unmarshal([]byte(input), &c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal initial input")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal unmarshalled input")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := compareMarshal(input, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
96
stanza/component.go
Normal file
96
stanza/component.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Handshake Stanza
|
||||||
|
|
||||||
|
// Handshake is a stanza used by XMPP components to authenticate on XMPP
|
||||||
|
// component port.
|
||||||
|
type Handshake struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||||
|
// TODO Add handshake value with test for proper serialization
|
||||||
|
Value string `xml:",innerxml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Handshake) Name() string {
|
||||||
|
return "component:handshake"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handshake decoding wrapper
|
||||||
|
|
||||||
|
type handshakeDecoder struct{}
|
||||||
|
|
||||||
|
var handshake handshakeDecoder
|
||||||
|
|
||||||
|
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
|
||||||
|
var packet Handshake
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Component delegation
|
||||||
|
// XEP-0355
|
||||||
|
|
||||||
|
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
|
||||||
|
// depending on the context.
|
||||||
|
type Delegation struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
|
||||||
|
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
|
||||||
|
Delegated *Delegated // This is used in a message to confirm delegated namespace
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Delegation) Namespace() string {
|
||||||
|
return d.XMLName.Space
|
||||||
|
}
|
||||||
|
func (d *Delegation) GetSet() *ResultSet {
|
||||||
|
return d.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forwarded is used to wrapped forwarded stanzas.
|
||||||
|
// TODO: Move it in another file, as it is not limited to components.
|
||||||
|
type Forwarded struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
|
||||||
|
Stanza Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||||
|
// transform generic XML content into hierarchical Node structure.
|
||||||
|
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
// Check subelements to extract required field as boolean
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tt := t.(type) {
|
||||||
|
|
||||||
|
case xml.StartElement:
|
||||||
|
if packet, err := decodeClient(d, tt); err == nil {
|
||||||
|
f.Stanza = packet
|
||||||
|
}
|
||||||
|
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Delegated struct {
|
||||||
|
XMLName xml.Name `xml:"delegated"`
|
||||||
|
Namespace string `xml:"namespace,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
|
||||||
|
}
|
79
stanza/component_test.go
Normal file
79
stanza/component_test.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// We should be able to properly parse delegation confirmation messages
|
||||||
|
func TestParsingDelegationMessage(t *testing.T) {
|
||||||
|
packetStr := `<message to='service.localhost' from='localhost'>
|
||||||
|
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||||
|
<delegated namespace='http://jabber.org/protocol/pubsub'/>
|
||||||
|
</delegation>
|
||||||
|
</message>`
|
||||||
|
var msg Message
|
||||||
|
data := []byte(packetStr)
|
||||||
|
if err := xml.Unmarshal(data, &msg); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have extracted the delegation info as MsgExtension
|
||||||
|
var nsDelegated string
|
||||||
|
for _, ext := range msg.Extensions {
|
||||||
|
if delegation, ok := ext.(*Delegation); ok {
|
||||||
|
nsDelegated = delegation.Delegated.Namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nsDelegated != "http://jabber.org/protocol/pubsub" {
|
||||||
|
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we can parse a delegation IQ.
|
||||||
|
// The most important thing is to be able to
|
||||||
|
func TestParsingDelegationIQ(t *testing.T) {
|
||||||
|
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
|
||||||
|
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||||
|
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||||
|
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
|
||||||
|
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||||
|
<publish node='http://jabber.org/protocol/mood'>
|
||||||
|
<item id='current'>
|
||||||
|
<mood xmlns='http://jabber.org/protocol/mood'>
|
||||||
|
<excited/>
|
||||||
|
</mood>
|
||||||
|
</item>
|
||||||
|
</publish>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
</forwarded>
|
||||||
|
</delegation>
|
||||||
|
</iq>`
|
||||||
|
var iq IQ
|
||||||
|
data := []byte(packetStr)
|
||||||
|
if err := xml.Unmarshal(data, &iq); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have extracted the delegation info as IQPayload
|
||||||
|
var node string
|
||||||
|
if iq.Payload != nil {
|
||||||
|
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||||
|
packet := delegation.Forwarded.Stanza
|
||||||
|
forwardedIQ, ok := packet.(*IQ)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Could not extract packet IQ")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if forwardedIQ.Payload != nil {
|
||||||
|
if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); ok {
|
||||||
|
node = pubsub.Publish.Node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node != "http://jabber.org/protocol/mood" {
|
||||||
|
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
|
||||||
|
}
|
||||||
|
}
|
70
stanza/datetime_profiles.go
Normal file
70
stanza/datetime_profiles.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper structures and functions to manage dates and timestamps as defined in
|
||||||
|
// XEP-0082: XMPP Date and Time Profiles (https://xmpp.org/extensions/xep-0082.html)
|
||||||
|
|
||||||
|
const dateLayoutXEP0082 = "2006-01-02"
|
||||||
|
const timeLayoutXEP0082 = "15:04:05+00:00"
|
||||||
|
|
||||||
|
var InvalidDateInput = errors.New("could not parse date. Input might not be in a supported format")
|
||||||
|
var InvalidDateOutput = errors.New("could not format date as desired")
|
||||||
|
|
||||||
|
type JabberDate struct {
|
||||||
|
value time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d JabberDate) DateToString() string {
|
||||||
|
return d.value.Format(dateLayoutXEP0082)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d JabberDate) DateTimeToString(nanos bool) string {
|
||||||
|
if nanos {
|
||||||
|
return d.value.Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
return d.value.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d JabberDate) TimeToString(nanos bool) (string, error) {
|
||||||
|
if nanos {
|
||||||
|
spl := strings.Split(d.value.Format(time.RFC3339Nano), "T")
|
||||||
|
if len(spl) != 2 {
|
||||||
|
return "", InvalidDateOutput
|
||||||
|
}
|
||||||
|
return spl[1], nil
|
||||||
|
}
|
||||||
|
spl := strings.Split(d.value.Format(time.RFC3339), "T")
|
||||||
|
if len(spl) != 2 {
|
||||||
|
return "", InvalidDateOutput
|
||||||
|
}
|
||||||
|
return spl[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJabberDateFromString(strDate string) (JabberDate, error) {
|
||||||
|
t, err := time.Parse(time.RFC3339, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse(time.RFC3339Nano, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse(dateLayoutXEP0082, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse(timeLayoutXEP0082, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return JabberDate{}, InvalidDateInput
|
||||||
|
}
|
191
stanza/datetime_profiles_test.go
Normal file
191
stanza/datetime_profiles_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDateToString(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
t2 := JabberDate{value: time.Now().Add(24 * time.Hour)}
|
||||||
|
|
||||||
|
t1Str := t1.DateToString()
|
||||||
|
t2Str := t2.DateToString()
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateToStringOracle(t *testing.T) {
|
||||||
|
expected := "2009-11-10"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str := t1.DateToString()
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToString(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(false)
|
||||||
|
t2Str := t2.DateTimeToString(false)
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToStringOracle(t *testing.T) {
|
||||||
|
expected := "2009-11-10T23:03:22+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(false)
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToStringNanos(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
time.After(10 * time.Millisecond)
|
||||||
|
t2 := JabberDate{value: time.Now()}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(true)
|
||||||
|
t2Str := t2.DateTimeToString(true)
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToStringNanosOracle(t *testing.T) {
|
||||||
|
expected := "2009-11-10T23:03:22.000000089+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(true)
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeToString(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t2Str, err := t2.TimeToString(false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeToStringOracle(t *testing.T) {
|
||||||
|
expected := "23:03:22+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeToStringNanos(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
time.After(10 * time.Millisecond)
|
||||||
|
t2 := JabberDate{value: time.Now()}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t2Str, err := t2.TimeToString(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestTimeToStringNanosOracle(t *testing.T) {
|
||||||
|
expected := "23:03:22.000000089+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJabberDateParsing(t *testing.T) {
|
||||||
|
date := "2009-11-10"
|
||||||
|
_, err := NewJabberDateFromString(date)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTime := "2009-11-10T23:03:22+08:00"
|
||||||
|
_, err = NewJabberDateFromString(dateTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTimeNanos := "2009-11-10T23:03:22.000000089+08:00"
|
||||||
|
_, err = NewJabberDateFromString(dateTimeNanos)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : fix these. Parsing a time with an offset doesn't work
|
||||||
|
//time := "23:03:22+08:00"
|
||||||
|
//_, err = NewJabberDateFromString(time)
|
||||||
|
//if err != nil {
|
||||||
|
// t.Fatalf(err.Error())
|
||||||
|
//}
|
||||||
|
|
||||||
|
//timeNanos := "23:03:22.000000089+08:00"
|
||||||
|
//_, err = NewJabberDateFromString(timeNanos)
|
||||||
|
//if err != nil {
|
||||||
|
// t.Fatalf(err.Error())
|
||||||
|
//}
|
||||||
|
|
||||||
|
}
|
4
stanza/doc.go
Normal file
4
stanza/doc.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
XMPP stanza package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
||||||
|
*/
|
||||||
|
package stanza
|
136
stanza/error.go
Normal file
136
stanza/error.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// XMPP Errors
|
||||||
|
|
||||||
|
// Err is an XMPP stanza payload that is used to report error on message,
|
||||||
|
// presence or iq stanza.
|
||||||
|
// It is intended to be added in the payload of the erroneous stanza.
|
||||||
|
type Err struct {
|
||||||
|
XMLName xml.Name `xml:"error"`
|
||||||
|
Code int `xml:"code,attr,omitempty"`
|
||||||
|
Type ErrorType `xml:"type,attr"` // required
|
||||||
|
Reason string
|
||||||
|
Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements custom parsing for XMPP errors
|
||||||
|
func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
x.XMLName = start.Name
|
||||||
|
|
||||||
|
// Extract attributes
|
||||||
|
for _, attr := range start.Attr {
|
||||||
|
if attr.Name.Local == "type" {
|
||||||
|
x.Type = ErrorType(attr.Value)
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "code" {
|
||||||
|
if code, err := strconv.Atoi(attr.Value); err == nil {
|
||||||
|
x.Code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check subelements to extract error text and reason (from local namespace).
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tt := t.(type) {
|
||||||
|
|
||||||
|
case xml.StartElement:
|
||||||
|
elt := new(Node)
|
||||||
|
|
||||||
|
err = d.DecodeElement(elt, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||||
|
// TODO : change the pubsub handling ? It kind of dilutes the information
|
||||||
|
// Handles : 6.1.3.11 Node Has Moved for XEP-0060 (PubSubGeneric)
|
||||||
|
goneName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "gone"}
|
||||||
|
if elt.XMLName == textName || // Regular error text
|
||||||
|
elt.XMLName == goneName { // Gone text for pubsub
|
||||||
|
x.Text = elt.Content
|
||||||
|
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" ||
|
||||||
|
elt.XMLName.Space == "http://jabber.org/protocol/pubsub#errors" {
|
||||||
|
if strings.TrimSpace(x.Reason) != "" {
|
||||||
|
x.Reason = strings.Join([]string{elt.XMLName.Local}, ":")
|
||||||
|
} else {
|
||||||
|
x.Reason = elt.XMLName.Local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||||
|
if x.Code == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode start element and attributes
|
||||||
|
start.Name = xml.Name{Local: "error"}
|
||||||
|
|
||||||
|
code := xml.Attr{
|
||||||
|
Name: xml.Name{Local: "code"},
|
||||||
|
Value: strconv.Itoa(x.Code),
|
||||||
|
}
|
||||||
|
start.Attr = append(start.Attr, code)
|
||||||
|
|
||||||
|
if len(x.Type) > 0 {
|
||||||
|
typ := xml.Attr{
|
||||||
|
Name: xml.Name{Local: "type"},
|
||||||
|
Value: string(x.Type),
|
||||||
|
}
|
||||||
|
start.Attr = append(start.Attr, typ)
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(start)
|
||||||
|
|
||||||
|
// SubTags
|
||||||
|
// Reason
|
||||||
|
if x.Reason != "" {
|
||||||
|
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
|
||||||
|
err = e.EncodeToken(xml.StartElement{Name: reason})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.EndElement{Name: reason})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text
|
||||||
|
if x.Text != "" {
|
||||||
|
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||||
|
err = e.EncodeToken(xml.StartElement{Name: text})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.CharData(x.Text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.EndElement{Name: text})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||||
|
}
|
13
stanza/error_enum.go
Normal file
13
stanza/error_enum.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
// ErrorType is a Enum of error attribute type
|
||||||
|
type ErrorType string
|
||||||
|
|
||||||
|
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
||||||
|
const (
|
||||||
|
ErrorTypeAuth ErrorType = "auth"
|
||||||
|
ErrorTypeCancel ErrorType = "cancel"
|
||||||
|
ErrorTypeContinue ErrorType = "continue"
|
||||||
|
ErrorTypeModify ErrorType = "modify"
|
||||||
|
ErrorTypeWait ErrorType = "wait"
|
||||||
|
)
|
31
stanza/error_test.go
Normal file
31
stanza/error_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErr_UnmarshalXML(t *testing.T) {
|
||||||
|
packet := `
|
||||||
|
<iq from='pubsub.example.com'
|
||||||
|
id='kj4vz31m'
|
||||||
|
to='romeo@example.net/foo'
|
||||||
|
type='error'>
|
||||||
|
<error type='wait'>
|
||||||
|
<resource-constraint
|
||||||
|
xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>System overloaded, please retry</text>
|
||||||
|
</error>
|
||||||
|
</iq>`
|
||||||
|
|
||||||
|
parsedIQ := IQ{}
|
||||||
|
data := []byte(packet)
|
||||||
|
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
xmppError := parsedIQ.Error
|
||||||
|
if xmppError.Text != "System overloaded, please retry" {
|
||||||
|
t.Errorf("Could not extract error text: '%s'", xmppError.Text)
|
||||||
|
}
|
||||||
|
}
|
34
stanza/fifo_queue.go
Normal file
34
stanza/fifo_queue.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
// FIFO queue for string contents
|
||||||
|
// Implementations have no guarantee regarding thread safety !
|
||||||
|
type FifoQueue interface {
|
||||||
|
// Pop returns the first inserted element still in queue and deletes it from queue. If queue is empty, returns nil
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Pop() Queueable
|
||||||
|
|
||||||
|
// PopN returns the N first inserted elements still in queue and deletes them from queue. If queue is empty or i<=0, returns nil
|
||||||
|
// If number to pop is greater than queue length, returns all queue elements
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
PopN(i int) []Queueable
|
||||||
|
|
||||||
|
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty, returns nil
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Peek() Queueable
|
||||||
|
|
||||||
|
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty or i<=0, returns nil.
|
||||||
|
// If number to peek is greater than queue length, returns all queue elements
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
PeekN() []Queueable
|
||||||
|
// Push adds an element to the queue
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Push(s Queueable) error
|
||||||
|
|
||||||
|
// Empty returns true if queue is empty
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Empty() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queueable interface {
|
||||||
|
QueueableName() string
|
||||||
|
}
|
68
stanza/form.go
Normal file
68
stanza/form.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
type FormType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormTypeCancel = "cancel"
|
||||||
|
FormTypeForm = "form"
|
||||||
|
FormTypeResult = "result"
|
||||||
|
FormTypeSubmit = "submit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// See XEP-0004 and XEP-0068
|
||||||
|
// Pointer semantics
|
||||||
|
type Form struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:x:data x"`
|
||||||
|
Instructions []string `xml:"instructions"`
|
||||||
|
Title string `xml:"title,omitempty"`
|
||||||
|
Fields []*Field `xml:"field,omitempty"`
|
||||||
|
Reported *FormItem `xml:"reported"`
|
||||||
|
Items []FormItem `xml:"item,omitempty"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItem struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Fields []Field `xml:"field,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Field struct {
|
||||||
|
XMLName xml.Name `xml:"field"`
|
||||||
|
Description string `xml:"desc,omitempty"`
|
||||||
|
Required *string `xml:"required"`
|
||||||
|
ValuesList []string `xml:"value"`
|
||||||
|
Options []Option `xml:"option,omitempty"`
|
||||||
|
Var string `xml:"var,attr,omitempty"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
Label string `xml:"label,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewForm(fields []*Field, formType string) *Form {
|
||||||
|
return &Form{
|
||||||
|
Type: formType,
|
||||||
|
Fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldTypeBool = "boolean"
|
||||||
|
FieldTypeFixed = "fixed"
|
||||||
|
FieldTypeHidden = "hidden"
|
||||||
|
FieldTypeJidMulti = "jid-multi"
|
||||||
|
FieldTypeJidSingle = "jid-single"
|
||||||
|
FieldTypeListMulti = "list-multi"
|
||||||
|
FieldTypeListSingle = "list-single"
|
||||||
|
FieldTypeTextMulti = "text-multi"
|
||||||
|
FieldTypeTextPrivate = "text-private"
|
||||||
|
FieldTypeTextSingle = "text-Single"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
XMLName xml.Name `xml:"option"`
|
||||||
|
Label string `xml:"label,attr,omitempty"`
|
||||||
|
ValuesList []string `xml:"value"`
|
||||||
|
}
|
110
stanza/form_test.go
Normal file
110
stanza/form_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
formSubmit = "<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\">" +
|
||||||
|
"<configure node=\"princely_musings\">" +
|
||||||
|
"<x xmlns=\"jabber:x:data\" type=\"submit\">" +
|
||||||
|
"<field var=\"FORM_TYPE\" type=\"hidden\">" +
|
||||||
|
"<value>http://jabber.org/protocol/pubsub#node_config</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#title\">" +
|
||||||
|
"<value>Princely Musings (Atom)</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#deliver_notifications\">" +
|
||||||
|
"<value>1</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#access_model\">" +
|
||||||
|
"<value>roster</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#roster_groups_allowed\">" +
|
||||||
|
"<value>friends</value>" +
|
||||||
|
"<value>servants</value>" +
|
||||||
|
"<value>courtiers</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#type\">" +
|
||||||
|
"<value>http://www.w3.org/2005/Atom</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#notification_type\" type=\"list-single\"" +
|
||||||
|
"label=\"Specify the delivery style for event notifications\">" +
|
||||||
|
"<value>headline</value>" +
|
||||||
|
"<option>" +
|
||||||
|
"<value>normal</value>" +
|
||||||
|
"</option>" +
|
||||||
|
"<option>" +
|
||||||
|
"<value>headline</value>" +
|
||||||
|
"</option>" +
|
||||||
|
"</field>" +
|
||||||
|
"</x>" +
|
||||||
|
"</configure>" +
|
||||||
|
"</pubsub>"
|
||||||
|
|
||||||
|
clientJid = "hamlet@denmark.lit/elsinore"
|
||||||
|
serviceJid = "pubsub.shakespeare.lit"
|
||||||
|
iqId = "config1"
|
||||||
|
serviceNode = "princely_musings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalFormSubmit(t *testing.T) {
|
||||||
|
formIQ, err := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create formIQ: %v", err)
|
||||||
|
}
|
||||||
|
formIQ.Payload = &PubSubOwner{
|
||||||
|
OwnerUseCase: &ConfigureOwner{
|
||||||
|
Node: serviceNode,
|
||||||
|
Form: &Form{
|
||||||
|
Type: FormTypeSubmit,
|
||||||
|
Fields: []*Field{
|
||||||
|
{Var: "FORM_TYPE", Type: FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||||
|
{Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}},
|
||||||
|
{Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}},
|
||||||
|
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
|
||||||
|
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
|
||||||
|
{Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}},
|
||||||
|
{
|
||||||
|
Var: "pubsub#notification_type",
|
||||||
|
Type: "list-single",
|
||||||
|
Label: "Specify the delivery style for event notifications",
|
||||||
|
ValuesList: []string{"headline"},
|
||||||
|
Options: []Option{
|
||||||
|
{ValuesList: []string{"normal"}},
|
||||||
|
{ValuesList: []string{"headline"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := xml.Marshal(formIQ.Payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not marshal formIQ : %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ReplaceAll(string(b), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
|
||||||
|
t.Fatalf("Expected formIQ and marshalled one are different.\nExepected : %s\nMarshalled : %s", formSubmit, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalFormSubmit(t *testing.T) {
|
||||||
|
var f PubSubOwner
|
||||||
|
mErr := xml.Unmarshal([]byte(formSubmit), &f)
|
||||||
|
if mErr != nil {
|
||||||
|
t.Fatalf("failed to unmarshal formSubmit ! %s", mErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(&f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal formSubmit")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ReplaceAll(string(data), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
|
||||||
|
t.Fatalf("failed unmarshal/marshal for formSubmit : %s\n%s", string(data), formSubmit)
|
||||||
|
}
|
||||||
|
}
|
48
stanza/iot.go
Normal file
48
stanza/iot.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ControlSet struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
|
||||||
|
Fields []ControlField `xml:",any"`
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControlSet) Namespace() string {
|
||||||
|
return c.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControlSet) GetSet() *ResultSet {
|
||||||
|
return c.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlGetForm struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlField struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Name string `xml:"name,attr,omitempty"`
|
||||||
|
Value string `xml:"value,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlSetResponse struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControlSetResponse) Namespace() string {
|
||||||
|
return c.XMLName.Space
|
||||||
|
}
|
||||||
|
func (c *ControlSetResponse) GetSet() *ResultSet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Registry init
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:iot:control", Local: "set"}, ControlSet{})
|
||||||
|
}
|
26
stanza/iot_test.go
Normal file
26
stanza/iot_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestControlSet(t *testing.T) {
|
||||||
|
packet := `
|
||||||
|
<iq to='test@localhost/jukebox' from='admin@localhost/mbp' type='set' id='2'>
|
||||||
|
<set xmlns='urn:xmpp:iot:control' xml:lang='en'>
|
||||||
|
<string name='action' value='play'/>
|
||||||
|
<string name='url' value='https://soundcloud.com/radiohead/spectre'/>
|
||||||
|
</set>
|
||||||
|
</iq>`
|
||||||
|
|
||||||
|
parsedIQ := IQ{}
|
||||||
|
data := []byte(packet)
|
||||||
|
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
|
||||||
|
t.Errorf("Payload is not an iot control set: %v", cs)
|
||||||
|
}
|
||||||
|
}
|
192
stanza/iq.go
Normal file
192
stanza/iq.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO support ability to put Raw payload inside IQ
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// IQ Packet
|
||||||
|
|
||||||
|
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
|
||||||
|
type IQ struct { // Info/Query
|
||||||
|
XMLName xml.Name `xml:"iq"`
|
||||||
|
// MUST have a ID
|
||||||
|
Attrs
|
||||||
|
// We can only have one payload on IQ:
|
||||||
|
// "An IQ stanza of type "get" or "set" MUST contain exactly one
|
||||||
|
// child element, which specifies the semantics of the particular
|
||||||
|
// request."
|
||||||
|
Payload IQPayload `xml:",omitempty"`
|
||||||
|
Error *Err `xml:"error,omitempty"`
|
||||||
|
// Any is used to decode unknown payload as a generic structure
|
||||||
|
Any *Node `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IQPayload interface {
|
||||||
|
Namespace() string
|
||||||
|
GetSet() *ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIQ(a Attrs) (*IQ, error) {
|
||||||
|
if a.Id == "" {
|
||||||
|
if id, err := uuid.NewRandom(); err == nil {
|
||||||
|
a.Id = id.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iq := IQ{
|
||||||
|
XMLName: xml.Name{Local: "iq"},
|
||||||
|
Attrs: a,
|
||||||
|
}
|
||||||
|
|
||||||
|
if iq.Type.IsEmpty() {
|
||||||
|
return nil, IqTypeUnset
|
||||||
|
}
|
||||||
|
return &iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iq *IQ) MakeError(xerror Err) *IQ {
|
||||||
|
from := iq.From
|
||||||
|
to := iq.To
|
||||||
|
|
||||||
|
iq.Type = "error"
|
||||||
|
iq.From = to
|
||||||
|
iq.To = from
|
||||||
|
iq.Error = &xerror
|
||||||
|
|
||||||
|
return iq
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*IQ) Name() string {
|
||||||
|
return "iq"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoOp to implement BiDirIteratorElt
|
||||||
|
func (*IQ) NoOp() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type iqDecoder struct{}
|
||||||
|
|
||||||
|
var iq iqDecoder
|
||||||
|
|
||||||
|
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (*IQ, error) {
|
||||||
|
var packet IQ
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return &packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements custom parsing for IQs
|
||||||
|
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
iq.XMLName = start.Name
|
||||||
|
|
||||||
|
// Extract IQ attributes
|
||||||
|
for _, attr := range start.Attr {
|
||||||
|
if attr.Name.Local == "id" {
|
||||||
|
iq.Id = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "type" {
|
||||||
|
iq.Type = StanzaType(attr.Value)
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "to" {
|
||||||
|
iq.To = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "from" {
|
||||||
|
iq.From = attr.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode inner elements
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tt := t.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
if tt.Name.Local == "error" {
|
||||||
|
var xmppError Err
|
||||||
|
err = d.DecodeElement(&xmppError, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iq.Error = &xmppError
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
||||||
|
// Decode payload extension
|
||||||
|
err = d.DecodeElement(iqExt, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iq.Payload = iqExt
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// TODO: If unknown decode as generic node
|
||||||
|
node := new(Node)
|
||||||
|
err = d.DecodeElement(node, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iq.Any = node
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
IqTypeUnset = errors.New("iq type is not set but is mandatory")
|
||||||
|
IqIDUnset = errors.New("iq stanza ID is not set but is mandatory")
|
||||||
|
IqSGetNoPl = errors.New("iq is of type get or set but has no payload")
|
||||||
|
IqResNoPl = errors.New("iq is of type result but has no payload")
|
||||||
|
IqErrNoErrPl = errors.New("iq is of type error but has no error payload")
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid checks if the IQ is valid. If not, return an error with the reason as a message
|
||||||
|
// Following RFC-3920 for IQs
|
||||||
|
func (iq *IQ) IsValid() (bool, error) {
|
||||||
|
// ID is required
|
||||||
|
if len(strings.TrimSpace(iq.Id)) == 0 {
|
||||||
|
return false, IqIDUnset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type is required
|
||||||
|
if iq.Type.IsEmpty() {
|
||||||
|
return false, IqTypeUnset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type get and set must contain one and only one child element that specifies the semantics
|
||||||
|
if iq.Type == IQTypeGet || iq.Type == IQTypeSet {
|
||||||
|
if iq.Payload == nil && iq.Any == nil {
|
||||||
|
return false, IqSGetNoPl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A result must include zero or one child element
|
||||||
|
if iq.Type == IQTypeResult {
|
||||||
|
if iq.Payload != nil && iq.Any != nil {
|
||||||
|
return false, IqResNoPl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Error type must contain an "error" child element
|
||||||
|
if iq.Type == IQTypeError {
|
||||||
|
if iq.Error == nil {
|
||||||
|
return false, IqErrNoErrPl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
163
stanza/iq_disco.go
Normal file
163
stanza/iq_disco.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Disco Info
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NSDiscoInfo defines the namespace for disco IQ stanzas
|
||||||
|
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------
|
||||||
|
// Namespaces
|
||||||
|
|
||||||
|
type DiscoInfo struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Identity []Identity `xml:"identity"`
|
||||||
|
Features []Feature `xml:"feature"`
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace lets DiscoInfo implement the IQPayload interface
|
||||||
|
func (d *DiscoInfo) Namespace() string {
|
||||||
|
return d.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoInfo) GetSet() *ResultSet {
|
||||||
|
return d.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// DiscoInfo builds a default DiscoInfo payload
|
||||||
|
func (iq *IQ) DiscoInfo() *DiscoInfo {
|
||||||
|
d := DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: NSDiscoInfo,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iq.Payload = &d
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoInfo) AddIdentity(name, category, typ string) {
|
||||||
|
identity := Identity{
|
||||||
|
XMLName: xml.Name{Local: "identity"},
|
||||||
|
Name: name,
|
||||||
|
Category: category,
|
||||||
|
Type: typ,
|
||||||
|
}
|
||||||
|
d.Identity = append(d.Identity, identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoInfo) AddFeatures(namespace ...string) {
|
||||||
|
for _, ns := range namespace {
|
||||||
|
d.Features = append(d.Features, Feature{Var: ns})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoInfo) SetNode(node string) *DiscoInfo {
|
||||||
|
d.Node = node
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
|
||||||
|
d.Identity = ident
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
|
||||||
|
d.Features = []Feature{}
|
||||||
|
for _, ns := range namespace {
|
||||||
|
d.Features = append(d.Features, Feature{Var: ns})
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------
|
||||||
|
// SubElements
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
XMLName xml.Name `xml:"identity,omitempty"`
|
||||||
|
Name string `xml:"name,attr,omitempty"`
|
||||||
|
Category string `xml:"category,attr,omitempty"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feature struct {
|
||||||
|
XMLName xml.Name `xml:"feature"`
|
||||||
|
Var string `xml:"var,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Disco Info
|
||||||
|
|
||||||
|
const (
|
||||||
|
NSDiscoItems = "http://jabber.org/protocol/disco#items"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscoItems struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Items []DiscoItem `xml:"item"`
|
||||||
|
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoItems) Namespace() string {
|
||||||
|
return d.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoItems) GetSet() *ResultSet {
|
||||||
|
return d.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// DiscoItems builds a default DiscoItems payload
|
||||||
|
func (iq *IQ) DiscoItems() *DiscoItems {
|
||||||
|
d := DiscoItems{
|
||||||
|
XMLName: xml.Name{Space: NSDiscoItems, Local: "query"},
|
||||||
|
}
|
||||||
|
iq.Payload = &d
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoItems) SetNode(node string) *DiscoItems {
|
||||||
|
d.Node = node
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoItems) AddItem(jid, node, name string) *DiscoItems {
|
||||||
|
item := DiscoItem{
|
||||||
|
JID: jid,
|
||||||
|
Node: node,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
d.Items = append(d.Items, item)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoItem struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
JID string `xml:"jid,attr,omitempty"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Name string `xml:"name,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Registry init
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoInfo, Local: "query"}, DiscoInfo{})
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoItems, Local: "query"}, DiscoItems{})
|
||||||
|
}
|
96
stanza/iq_disco_test.go
Normal file
96
stanza/iq_disco_test.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test DiscoInfo Builder with several features
|
||||||
|
func TestDiscoInfo_Builder(t *testing.T) {
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
disco := iq.DiscoInfo()
|
||||||
|
disco.AddIdentity("Test Component", "gateway", "service")
|
||||||
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
|
||||||
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check features
|
||||||
|
features := []string{stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"}
|
||||||
|
if len(pp.Features) != len(features) {
|
||||||
|
t.Errorf("Features length mismatch: %#v", pp.Features)
|
||||||
|
} else {
|
||||||
|
for i, f := range pp.Features {
|
||||||
|
if f.Var != features[i] {
|
||||||
|
t.Errorf("Missing feature: %s", features[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check identity
|
||||||
|
if len(pp.Identity) != 1 {
|
||||||
|
t.Errorf("Identity length mismatch: %#v", pp.Identity)
|
||||||
|
} else {
|
||||||
|
if pp.Identity[0].Name != "Test Component" {
|
||||||
|
t.Errorf("Incorrect identity name: %#v", pp.Identity[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements XEP-0030 example 17
|
||||||
|
// https://xmpp.org/extensions/xep-0030.html#example-17
|
||||||
|
func TestDiscoItems_Builder(t *testing.T) {
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit",
|
||||||
|
To: "romeo@montague.net/orchard", Id: "items-2"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
iq.DiscoItems().
|
||||||
|
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
|
||||||
|
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
|
||||||
|
AddItem("catalog.shakespeare.lit", "music", "Music from the time of Shakespeare")
|
||||||
|
|
||||||
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
pp, ok := parsedIQ.Payload.(*stanza.DiscoItems)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check items
|
||||||
|
items := []stanza.DiscoItem{{xml.Name{}, "catalog.shakespeare.lit", "books", "Books by and about Shakespeare"},
|
||||||
|
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
|
||||||
|
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
|
||||||
|
if len(pp.Items) != len(items) {
|
||||||
|
t.Errorf("List length mismatch: %#v", pp.Items)
|
||||||
|
} else {
|
||||||
|
for i, item := range pp.Items {
|
||||||
|
if item.JID != items[i].JID {
|
||||||
|
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
|
}
|
||||||
|
if item.Node != items[i].Node {
|
||||||
|
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
|
}
|
||||||
|
if item.Name != items[i].Name {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
126
stanza/iq_roster.go
Normal file
126
stanza/iq_roster.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Roster
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NSRoster is the Roster IQ namespace
|
||||||
|
NSRoster = "jabber:iq:roster"
|
||||||
|
// SubscriptionNone indicates the user does not have a subscription to
|
||||||
|
// the contact's presence, and the contact does not have a subscription
|
||||||
|
// to the user's presence; this is the default value, so if the subscription
|
||||||
|
// attribute is not included then the state is to be understood as "none"
|
||||||
|
SubscriptionNone = "none"
|
||||||
|
|
||||||
|
// SubscriptionTo indicates the user has a subscription to the contact's
|
||||||
|
// presence, but the contact does not have a subscription to the user's presence.
|
||||||
|
SubscriptionTo = "to"
|
||||||
|
|
||||||
|
// SubscriptionFrom indicates the contact has a subscription to the user's
|
||||||
|
// presence, but the user does not have a subscription to the contact's presence
|
||||||
|
SubscriptionFrom = "from"
|
||||||
|
|
||||||
|
// SubscriptionBoth indicates the user and the contact have subscriptions to each
|
||||||
|
// other's presence (also called a "mutual subscription")
|
||||||
|
SubscriptionBoth = "both"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------
|
||||||
|
// Namespaces
|
||||||
|
|
||||||
|
// Roster struct represents Roster IQs
|
||||||
|
type Roster struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:iq:roster query"`
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace defines the namespace for the RosterIQ
|
||||||
|
func (r *Roster) Namespace() string {
|
||||||
|
return r.XMLName.Space
|
||||||
|
}
|
||||||
|
func (r *Roster) GetSet() *ResultSet {
|
||||||
|
return r.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// RosterIQ builds a default Roster payload
|
||||||
|
func (iq *IQ) RosterIQ() *Roster {
|
||||||
|
r := Roster{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: NSRoster,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iq.Payload = &r
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------
|
||||||
|
// SubElements
|
||||||
|
|
||||||
|
// RosterItems represents the list of items in a roster IQ
|
||||||
|
type RosterItems struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:iq:roster query"`
|
||||||
|
Items []RosterItem `xml:"item"`
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace lets RosterItems implement the IQPayload interface
|
||||||
|
func (r *RosterItems) Namespace() string {
|
||||||
|
return r.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RosterItems) GetSet() *ResultSet {
|
||||||
|
return r.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// RosterItem represents an item in the roster iq
|
||||||
|
type RosterItem struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:iq:roster item"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
Ask string `xml:"ask,attr,omitempty"`
|
||||||
|
Name string `xml:"name,attr,omitempty"`
|
||||||
|
Subscription string `xml:"subscription,attr,omitempty"`
|
||||||
|
Groups []string `xml:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// RosterItems builds a default RosterItems payload
|
||||||
|
func (iq *IQ) RosterItems() *RosterItems {
|
||||||
|
ri := RosterItems{
|
||||||
|
XMLName: xml.Name{Space: "jabber:iq:roster", Local: "query"},
|
||||||
|
}
|
||||||
|
iq.Payload = &ri
|
||||||
|
return &ri
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddItem builds an item and ads it to the roster IQ
|
||||||
|
func (r *RosterItems) AddItem(jid, subscription, ask, name string, groups []string) *RosterItems {
|
||||||
|
item := RosterItem{
|
||||||
|
Jid: jid,
|
||||||
|
Name: name,
|
||||||
|
Groups: groups,
|
||||||
|
Subscription: subscription,
|
||||||
|
Ask: ask,
|
||||||
|
}
|
||||||
|
r.Items = append(r.Items, item)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Registry init
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, Roster{})
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, RosterItems{})
|
||||||
|
}
|
112
stanza/iq_roster_test.go
Normal file
112
stanza/iq_roster_test.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRosterBuilder(t *testing.T) {
|
||||||
|
iq, err := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
var noGroup []string
|
||||||
|
|
||||||
|
iq.RosterItems().AddItem("xl8ceawrfu8zdneomw1h6h28d@crypho.com",
|
||||||
|
SubscriptionBoth,
|
||||||
|
"",
|
||||||
|
"xl8ceaw",
|
||||||
|
[]string{"0flucpm8i2jtrjhxw01uf1nd2",
|
||||||
|
"bm2bajg9ex4e1swiuju9i9nu5",
|
||||||
|
"rvjpanomi4ejpx42fpmffoac0"}).
|
||||||
|
AddItem("9aynsym60zbu78jbdvpho7s68@crypho.com",
|
||||||
|
SubscriptionBoth,
|
||||||
|
"",
|
||||||
|
"9aynsym60",
|
||||||
|
[]string{"mzaoy73i6ra5k502182zi1t97"}).
|
||||||
|
AddItem("admin@crypho.com",
|
||||||
|
SubscriptionBoth,
|
||||||
|
"",
|
||||||
|
"admin",
|
||||||
|
noGroup)
|
||||||
|
|
||||||
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
pp, ok := parsedIQ.Payload.(*RosterItems)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check items
|
||||||
|
items := []RosterItem{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Name: "xl8ceaw",
|
||||||
|
Ask: "",
|
||||||
|
Jid: "xl8ceawrfu8zdneomw1h6h28d@crypho.com",
|
||||||
|
Subscription: SubscriptionBoth,
|
||||||
|
Groups: []string{"0flucpm8i2jtrjhxw01uf1nd2",
|
||||||
|
"bm2bajg9ex4e1swiuju9i9nu5",
|
||||||
|
"rvjpanomi4ejpx42fpmffoac0"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Name: "9aynsym60",
|
||||||
|
Ask: "",
|
||||||
|
Jid: "9aynsym60zbu78jbdvpho7s68@crypho.com",
|
||||||
|
Subscription: SubscriptionBoth,
|
||||||
|
Groups: []string{"mzaoy73i6ra5k502182zi1t97"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Name: "admin",
|
||||||
|
Ask: "",
|
||||||
|
Jid: "admin@crypho.com",
|
||||||
|
Subscription: SubscriptionBoth,
|
||||||
|
Groups: noGroup,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if len(pp.Items) != len(items) {
|
||||||
|
t.Errorf("List length mismatch: %#v", pp.Items)
|
||||||
|
} else {
|
||||||
|
for i, item := range pp.Items {
|
||||||
|
if item.Jid != items[i].Jid {
|
||||||
|
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(item.Groups, items[i].Groups) {
|
||||||
|
t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if item.Name != items[i].Name {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if item.Ask != items[i].Ask {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if item.Subscription != items[i].Subscription {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMarshalling(t *testing.T, iq *IQ) (*IQ, error) {
|
||||||
|
// Marshall
|
||||||
|
data, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshall
|
||||||
|
var parsedIQ IQ
|
||||||
|
err = xml.Unmarshal(data, &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
|
||||||
|
}
|
||||||
|
return &parsedIQ, err
|
||||||
|
}
|
240
stanza/iq_test.go
Normal file
240
stanza/iq_test.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalIqs(t *testing.T) {
|
||||||
|
//var cs1 = new(iot.ControlSet)
|
||||||
|
var tests = []struct {
|
||||||
|
iqString string
|
||||||
|
parsedIQ stanza.IQ
|
||||||
|
}{
|
||||||
|
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
|
||||||
|
stanza.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: stanza.Attrs{Type: stanza.IQTypeSet, To: "test@localhost", Id: "1"}}},
|
||||||
|
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", test.iqString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !xmlEqual(parsedIQ, test.parsedIQ) {
|
||||||
|
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ, test.parsedIQ))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateIqId(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Id: "1", Type: "dummy type"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
if iq.Id != "1" {
|
||||||
|
t.Errorf("NewIQ replaced id with %s", iq.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
iq, err = stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
if iq.Id == "" {
|
||||||
|
t.Error("NewIQ did not generate an Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
otherIq, err := stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
if iq.Id == otherIq.Id {
|
||||||
|
t.Errorf("NewIQ generated two identical ids: %s", iq.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateIq(t *testing.T) {
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
payload := stanza.DiscoInfo{
|
||||||
|
Identity: []stanza.Identity{
|
||||||
|
{Name: "Test Gateway",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "mqtt",
|
||||||
|
}},
|
||||||
|
Features: []stanza.Feature{
|
||||||
|
{Var: stanza.NSDiscoInfo},
|
||||||
|
{Var: stanza.NSDiscoItems},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iq.Payload = &payload
|
||||||
|
|
||||||
|
data, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot marshal xml structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(string(data), "<error ") {
|
||||||
|
t.Error("empty error should not be serialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
|
||||||
|
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorTag(t *testing.T) {
|
||||||
|
xError := stanza.Err{
|
||||||
|
XMLName: xml.Name{Local: "error"},
|
||||||
|
Code: 503,
|
||||||
|
Type: "cancel",
|
||||||
|
Reason: "service-unavailable",
|
||||||
|
Text: "User session not found",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(xError)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot marshal xml structure: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedError := stanza.Err{}
|
||||||
|
if err = xml.Unmarshal(data, &parsedError); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !xmlEqual(parsedError, xError) {
|
||||||
|
t.Errorf("non matching items\n%s", cmp.Diff(parsedError, xError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoItems(t *testing.T) {
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
payload := stanza.DiscoItems{
|
||||||
|
Node: "music",
|
||||||
|
}
|
||||||
|
iq.Payload = &payload
|
||||||
|
|
||||||
|
data, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot marshal xml structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !xmlEqual(parsedIQ.Payload, iq.Payload) {
|
||||||
|
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalPayload(t *testing.T) {
|
||||||
|
query := "<iq to='service.localhost' type='get' id='1'><query xmlns='jabber:iq:version'/></iq>"
|
||||||
|
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
err := xml.Unmarshal([]byte(query), &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedIQ.Payload == nil {
|
||||||
|
t.Error("Missing payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := parsedIQ.Payload.Namespace()
|
||||||
|
if namespace != "jabber:iq:version" {
|
||||||
|
t.Errorf("incorrect namespace: %s", namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayloadWithError(t *testing.T) {
|
||||||
|
iq := `<iq xml:lang='en' to='test1@localhost/resource' from='test@localhost' type='error' id='aac1a'>
|
||||||
|
<query xmlns='jabber:iq:version'/>
|
||||||
|
<error code='407' type='auth'>
|
||||||
|
<subscription-required xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
<text xml:lang='en' xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Not subscribed</text>
|
||||||
|
</error>
|
||||||
|
</iq>`
|
||||||
|
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
err := xml.Unmarshal([]byte(iq), &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal error: %s", iq)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedIQ.Error.Reason != "subscription-required" {
|
||||||
|
t.Errorf("incorrect error value: '%s'", parsedIQ.Error.Reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownPayload(t *testing.T) {
|
||||||
|
iq := `<iq type="get" to="service.localhost" id="1" >
|
||||||
|
<query xmlns="unknown:ns"/>
|
||||||
|
</iq>`
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
err := xml.Unmarshal([]byte(iq), &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal error: %#v (%s)", err, iq)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedIQ.Any.XMLName.Space != "unknown:ns" {
|
||||||
|
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsValid(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
iq string
|
||||||
|
shouldErr bool
|
||||||
|
}
|
||||||
|
testIQs := make(map[string]testCase)
|
||||||
|
testIQs["Valid IQ"] = testCase{
|
||||||
|
`<iq type="get" to="service.localhost" id="1" >
|
||||||
|
<query xmlns="unknown:ns"/>
|
||||||
|
</iq>`,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
testIQs["Invalid IQ"] = testCase{
|
||||||
|
`<iq type="get" to="service.localhost">
|
||||||
|
<query xmlns="unknown:ns"/>
|
||||||
|
</iq>`,
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tcase := range testIQs {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
err := xml.Unmarshal([]byte(tcase.iq), &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isValid, err := parsedIQ.IsValid()
|
||||||
|
if !isValid && !tcase.shouldErr {
|
||||||
|
t.Errorf("failed validation for iq because: %s\nin test case : %s", err, tcase.iq)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
51
stanza/iq_version.go
Normal file
51
stanza/iq_version.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Software Version (XEP-0092)
|
||||||
|
|
||||||
|
// Version
|
||||||
|
type Version struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:iq:version query"`
|
||||||
|
Name string `xml:"name,omitempty"`
|
||||||
|
Version string `xml:"version,omitempty"`
|
||||||
|
OS string `xml:"os,omitempty"`
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Version) Namespace() string {
|
||||||
|
return v.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Version) GetSet() *ResultSet {
|
||||||
|
return v.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// Version builds a default software version payload
|
||||||
|
func (iq *IQ) Version() *Version {
|
||||||
|
d := Version{
|
||||||
|
XMLName: xml.Name{Space: "jabber:iq:version", Local: "query"},
|
||||||
|
}
|
||||||
|
iq.Payload = &d
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set all software version info
|
||||||
|
func (v *Version) SetInfo(name, version, os string) *Version {
|
||||||
|
v.Name = name
|
||||||
|
v.Version = version
|
||||||
|
v.OS = os
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Registry init
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "jabber:iq:version", Local: "query"}, Version{})
|
||||||
|
}
|
43
stanza/iq_version_test.go
Normal file
43
stanza/iq_version_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build a Software Version reply
|
||||||
|
// https://xmpp.org/extensions/xep-0092.html#example-2
|
||||||
|
func TestVersion_Builder(t *testing.T) {
|
||||||
|
name := "Exodus"
|
||||||
|
version := "0.7.0.4"
|
||||||
|
os := "Windows-XP 5.01.2600"
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
|
||||||
|
To: "juliet@capulet.com/balcony", Id: "version_1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
iq.Version().SetInfo(name, version, os)
|
||||||
|
|
||||||
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
pp, ok := parsedIQ.Payload.(*stanza.Version)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version info
|
||||||
|
if pp.Name != name {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", name, pp.Name)
|
||||||
|
}
|
||||||
|
if pp.Version != version {
|
||||||
|
t.Errorf("Version Mismatch (expected: %s): %s", version, pp.Version)
|
||||||
|
}
|
||||||
|
if pp.OS != os {
|
||||||
|
t.Errorf("OS Mismatch (expected: %s): %s", os, pp.OS)
|
||||||
|
}
|
||||||
|
}
|
101
stanza/jid.go
Normal file
101
stanza/jid.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Jid struct {
|
||||||
|
Node string
|
||||||
|
Domain string
|
||||||
|
Resource string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJid(sjid string) (*Jid, error) {
|
||||||
|
jid := new(Jid)
|
||||||
|
|
||||||
|
if sjid == "" {
|
||||||
|
return jid, fmt.Errorf("jid cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
s1 := strings.SplitN(sjid, "@", 2)
|
||||||
|
if len(s1) == 1 { // This is a server or component Jid
|
||||||
|
jid.Domain = s1[0]
|
||||||
|
} else { // Jid has a local username part
|
||||||
|
if s1[0] == "" {
|
||||||
|
return jid, fmt.Errorf("invalid jid '%s", sjid)
|
||||||
|
}
|
||||||
|
jid.Node = s1[0]
|
||||||
|
if s1[1] == "" {
|
||||||
|
return jid, fmt.Errorf("domain cannot be empty")
|
||||||
|
}
|
||||||
|
jid.Domain = s1[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract resource from domain field
|
||||||
|
s2 := strings.SplitN(jid.Domain, "/", 2)
|
||||||
|
if len(s2) == 2 { // If len = 1, domain is already correct, and resource is already empty
|
||||||
|
jid.Domain = s2[0]
|
||||||
|
jid.Resource = s2[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isUsernameValid(jid.Node) {
|
||||||
|
return jid, fmt.Errorf("invalid Node in Jid '%s'", sjid)
|
||||||
|
}
|
||||||
|
if !isDomainValid(jid.Domain) {
|
||||||
|
return jid, fmt.Errorf("invalid domain in Jid '%s'", sjid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jid) Full() string {
|
||||||
|
if j.Resource == "" {
|
||||||
|
return j.Bare()
|
||||||
|
} else if j.Node == "" {
|
||||||
|
return j.Node + "/" + j.Resource
|
||||||
|
} else {
|
||||||
|
return j.Node + "@" + j.Domain + "/" + j.Resource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jid) Bare() string {
|
||||||
|
if j.Node == "" {
|
||||||
|
return j.Domain
|
||||||
|
} else {
|
||||||
|
return j.Node + "@" + j.Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helpers, for parsing / validation
|
||||||
|
|
||||||
|
func isUsernameValid(username string) bool {
|
||||||
|
invalidRunes := []rune{'@', '/', '\'', '"', ':', '<', '>'}
|
||||||
|
return strings.IndexFunc(username, isInvalid(invalidRunes)) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDomainValid(domain string) bool {
|
||||||
|
if len(domain) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidRunes := []rune{'@', '/'}
|
||||||
|
return strings.IndexFunc(domain, isInvalid(invalidRunes)) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInvalid(invalidRunes []rune) func(c rune) bool {
|
||||||
|
isInvalid := func(c rune) bool {
|
||||||
|
if unicode.IsSpace(c) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, r := range invalidRunes {
|
||||||
|
if c == r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isInvalid
|
||||||
|
}
|
101
stanza/jid_test.go
Normal file
101
stanza/jid_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidJids(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
jidstr string
|
||||||
|
expected Jid
|
||||||
|
}{
|
||||||
|
{jidstr: "test@domain.com", expected: Jid{"test", "domain.com", ""}},
|
||||||
|
{jidstr: "test@domain.com/resource", expected: Jid{"test", "domain.com", "resource"}},
|
||||||
|
// resource can contain '/' or '@'
|
||||||
|
{jidstr: "test@domain.com/a/b", expected: Jid{"test", "domain.com", "a/b"}},
|
||||||
|
{jidstr: "test@domain.com/a@b", expected: Jid{"test", "domain.com", "a@b"}},
|
||||||
|
{jidstr: "domain.com", expected: Jid{"", "domain.com", ""}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
jid, err := NewJid(tt.jidstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not parse correct jid: %s", tt.jidstr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if jid == nil {
|
||||||
|
t.Error("jid should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if jid.Node != tt.expected.Node {
|
||||||
|
t.Errorf("incorrect jid Node (%s): %s", tt.expected.Node, jid.Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jid.Node != tt.expected.Node {
|
||||||
|
t.Errorf("incorrect jid domain (%s): %s", tt.expected.Domain, jid.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jid.Resource != tt.expected.Resource {
|
||||||
|
t.Errorf("incorrect jid resource (%s): %s", tt.expected.Resource, jid.Resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIncorrectJids(t *testing.T) {
|
||||||
|
badJids := []string{
|
||||||
|
"",
|
||||||
|
"user@",
|
||||||
|
"@domain.com",
|
||||||
|
"user:name@domain.com",
|
||||||
|
"user<name@domain.com",
|
||||||
|
"test@domain.com@otherdomain.com",
|
||||||
|
"test@domain com/resource",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sjid := range badJids {
|
||||||
|
if _, err := NewJid(sjid); err == nil {
|
||||||
|
t.Error("parsing incorrect jid should return error: " + sjid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFull(t *testing.T) {
|
||||||
|
fullJids := []string{
|
||||||
|
"test@domain.com/my resource",
|
||||||
|
"test@domain.com",
|
||||||
|
"domain.com",
|
||||||
|
}
|
||||||
|
for _, sjid := range fullJids {
|
||||||
|
parsedJid, err := NewJid(sjid)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not parse jid: %v", err)
|
||||||
|
}
|
||||||
|
fullJid := parsedJid.Full()
|
||||||
|
if fullJid != sjid {
|
||||||
|
t.Errorf("incorrect full jid: %s", fullJid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBare(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
jidstr string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{jidstr: "test@domain.com", expected: "test@domain.com"},
|
||||||
|
{jidstr: "test@domain.com/resource", expected: "test@domain.com"},
|
||||||
|
{jidstr: "domain.com", expected: "domain.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
parsedJid, err := NewJid(tt.jidstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not parse jid: %v", err)
|
||||||
|
}
|
||||||
|
bareJid := parsedJid.Bare()
|
||||||
|
if bareJid != tt.expected {
|
||||||
|
t.Errorf("incorrect bare jid: %s", bareJid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
stanza/message.go
Normal file
148
stanza/message.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Message Packet
|
||||||
|
|
||||||
|
// Message implements RFC 6120 - A.5 Client Namespace (a part)
|
||||||
|
type Message struct {
|
||||||
|
XMLName xml.Name `xml:"message"`
|
||||||
|
Attrs
|
||||||
|
|
||||||
|
Subject string `xml:"subject,omitempty"`
|
||||||
|
Body string `xml:"body,omitempty"`
|
||||||
|
Thread string `xml:"thread,omitempty"`
|
||||||
|
Error Err `xml:"error,omitempty"`
|
||||||
|
Extensions []MsgExtension `xml:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Message) Name() string {
|
||||||
|
return "message"
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessage(a Attrs) Message {
|
||||||
|
return Message{
|
||||||
|
XMLName: xml.Name{Local: "message"},
|
||||||
|
Attrs: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get search and extracts a specific extension on a message.
|
||||||
|
// It receives a pointer to an MsgExtension. It will panic if the caller
|
||||||
|
// does not pass a pointer.
|
||||||
|
// It will return true if the passed extension is found and set the pointer
|
||||||
|
// to the extension passed as parameter to the found extension.
|
||||||
|
// It will return false if the extension is not found on the message.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
// var oob xmpp.OOB
|
||||||
|
// if ok := msg.Get(&oob); ok {
|
||||||
|
// // oob extension has been found
|
||||||
|
// }
|
||||||
|
func (msg *Message) Get(ext MsgExtension) bool {
|
||||||
|
target := reflect.ValueOf(ext)
|
||||||
|
if target.Kind() != reflect.Ptr {
|
||||||
|
panic("you must pass a pointer to the message Get method")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range msg.Extensions {
|
||||||
|
if reflect.TypeOf(e) == target.Type() {
|
||||||
|
source := reflect.ValueOf(e)
|
||||||
|
if source.Kind() != reflect.Ptr {
|
||||||
|
source = source.Elem()
|
||||||
|
}
|
||||||
|
target.Elem().Set(source.Elem())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageDecoder struct{}
|
||||||
|
|
||||||
|
var message messageDecoder
|
||||||
|
|
||||||
|
func (messageDecoder) decode(p *xml.Decoder, se xml.StartElement) (Message, error) {
|
||||||
|
var packet Message
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMPPFormat with all Extensions
|
||||||
|
func (msg *Message) XMPPFormat() string {
|
||||||
|
out, err := xml.MarshalIndent(msg, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements custom parsing for messages
|
||||||
|
func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
msg.XMLName = start.Name
|
||||||
|
|
||||||
|
// Extract packet attributes
|
||||||
|
for _, attr := range start.Attr {
|
||||||
|
if attr.Name.Local == "id" {
|
||||||
|
msg.Id = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "type" {
|
||||||
|
msg.Type = StanzaType(attr.Value)
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "to" {
|
||||||
|
msg.To = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "from" {
|
||||||
|
msg.From = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "lang" {
|
||||||
|
msg.Lang = attr.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode inner elements
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tt := t.(type) {
|
||||||
|
|
||||||
|
case xml.StartElement:
|
||||||
|
if msgExt := TypeRegistry.GetMsgExtension(tt.Name); msgExt != nil {
|
||||||
|
// Decode message extension
|
||||||
|
err = d.DecodeElement(msgExt, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg.Extensions = append(msg.Extensions, msgExt)
|
||||||
|
} else {
|
||||||
|
// Decode standard message sub-elements
|
||||||
|
var err error
|
||||||
|
switch tt.Name.Local {
|
||||||
|
case "body":
|
||||||
|
err = d.DecodeElement(&msg.Body, &tt)
|
||||||
|
case "thread":
|
||||||
|
err = d.DecodeElement(&msg.Thread, &tt)
|
||||||
|
case "subject":
|
||||||
|
err = d.DecodeElement(&msg.Subject, &tt)
|
||||||
|
case "error":
|
||||||
|
err = d.DecodeElement(&msg.Error, &tt)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
stanza/message_test.go
Normal file
76
stanza/message_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateMessage(t *testing.T) {
|
||||||
|
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||||
|
message.Body = "Hi"
|
||||||
|
message.Subject = "Msg Subject"
|
||||||
|
|
||||||
|
data, err := xml.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot marshal xml structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedMessage := stanza.Message{}
|
||||||
|
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !xmlEqual(parsedMessage, message) {
|
||||||
|
t.Errorf("non matching items\n%s", cmp.Diff(parsedMessage, message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeError(t *testing.T) {
|
||||||
|
str := `<message from='juliet@capulet.com'
|
||||||
|
id='msg_1'
|
||||||
|
to='romeo@montague.lit'
|
||||||
|
type='error'>
|
||||||
|
<error type='cancel'>
|
||||||
|
<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
</error>
|
||||||
|
</message>`
|
||||||
|
|
||||||
|
parsedMessage := stanza.Message{}
|
||||||
|
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||||
|
t.Errorf("message error stanza unmarshall error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if parsedMessage.Error.Type != "cancel" {
|
||||||
|
t.Errorf("incorrect error type: %s", parsedMessage.Error.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOOB(t *testing.T) {
|
||||||
|
image := "https://localhost/image.png"
|
||||||
|
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||||
|
ext := stanza.OOB{
|
||||||
|
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
|
||||||
|
URL: image,
|
||||||
|
}
|
||||||
|
msg.Extensions = append(msg.Extensions, &ext)
|
||||||
|
|
||||||
|
// OOB can properly be found
|
||||||
|
var oob stanza.OOB
|
||||||
|
// Try to find and
|
||||||
|
if ok := msg.Get(&oob); !ok {
|
||||||
|
t.Error("could not find oob extension")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if oob.URL != image {
|
||||||
|
t.Errorf("OOB URL was not properly extracted: ''%s", oob.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markable is not found
|
||||||
|
var m stanza.Markable
|
||||||
|
if ok := msg.Get(&m); ok {
|
||||||
|
t.Error("we should not have found markable extension")
|
||||||
|
}
|
||||||
|
}
|
42
stanza/msg_chat_markers.go
Normal file
42
stanza/msg_chat_markers.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Support for:
|
||||||
|
- XEP-0333 - Chat Markers: https://xmpp.org/extensions/xep-0333.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NSMsgChatMarkers = "urn:xmpp:chat-markers:0"
|
||||||
|
|
||||||
|
type Markable struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 markable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarkReceived struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 received"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarkDisplayed struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 displayed"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarkAcknowledged struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 acknowledged"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "markable"}, Markable{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "received"}, MarkReceived{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "displayed"}, MarkDisplayed{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "acknowledged"}, MarkAcknowledged{})
|
||||||
|
}
|
45
stanza/msg_chat_state.go
Normal file
45
stanza/msg_chat_state.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Support for:
|
||||||
|
- XEP-0085 - Chat State Notifications: https://xmpp.org/extensions/xep-0085.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NSMsgChatStateNotifications = "http://jabber.org/protocol/chatstates"
|
||||||
|
|
||||||
|
type StateActive struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateComposing struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates composing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateGone struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates gone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateInactive struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates inactive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatePaused struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates paused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "active"}, StateActive{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "composing"}, StateComposing{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "gone"}, StateGone{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "inactive"}, StateInactive{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "paused"}, StatePaused{})
|
||||||
|
}
|
36
stanza/msg_hint.go
Normal file
36
stanza/msg_hint.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Support for:
|
||||||
|
- XEP-0334: Message Processing Hints: https://xmpp.org/extensions/xep-0334.html
|
||||||
|
Pointers should be used to keep consistent with unmarshal. Eg :
|
||||||
|
msg.Extensions = append(msg.Extensions, &stanza.HintNoCopy{}, &stanza.HintStore{})
|
||||||
|
*/
|
||||||
|
|
||||||
|
type HintNoPermanentStore struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints no-permanent-store"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HintNoStore struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints no-store"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HintNoCopy struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints no-copy"`
|
||||||
|
}
|
||||||
|
type HintStore struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints store"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-permanent-store"}, HintNoPermanentStore{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-store"}, HintNoStore{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-copy"}, HintNoCopy{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "store"}, HintStore{})
|
||||||
|
}
|
72
stanza/msg_hint_test.go
Normal file
72
stanza/msg_hint_test.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const msg_const = `
|
||||||
|
<message
|
||||||
|
from="romeo@montague.lit/laptop"
|
||||||
|
to="juliet@capulet.lit/laptop">
|
||||||
|
<body>V unir avtugf pybnx gb uvqr zr sebz gurve fvtug</body>
|
||||||
|
<no-copy xmlns="urn:xmpp:hints"></no-copy>
|
||||||
|
<no-permanent-store xmlns="urn:xmpp:hints"></no-permanent-store>
|
||||||
|
<no-store xmlns="urn:xmpp:hints"></no-store>
|
||||||
|
<store xmlns="urn:xmpp:hints"></store>
|
||||||
|
</message>`
|
||||||
|
|
||||||
|
func TestSerializationHint(t *testing.T) {
|
||||||
|
msg := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
|
||||||
|
msg.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
|
||||||
|
msg.Extensions = append(msg.Extensions, stanza.HintNoCopy{}, stanza.HintNoPermanentStore{}, stanza.HintNoStore{}, stanza.HintStore{})
|
||||||
|
data, _ := xml.Marshal(msg)
|
||||||
|
if strings.ReplaceAll(strings.Join(strings.Fields(msg_const), ""), "\n", "") != strings.Join(strings.Fields(string(data)), "") {
|
||||||
|
t.Fatalf("marshalled message does not match expected message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalHints(t *testing.T) {
|
||||||
|
// Init message as in the const value
|
||||||
|
msgConst := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
|
||||||
|
msgConst.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
|
||||||
|
msgConst.Extensions = append(msgConst.Extensions, &stanza.HintNoCopy{}, &stanza.HintNoPermanentStore{}, &stanza.HintNoStore{}, &stanza.HintStore{})
|
||||||
|
|
||||||
|
// Compare message with the const value
|
||||||
|
msg := stanza.Message{}
|
||||||
|
err := xml.Unmarshal([]byte(msg_const), &msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msgConst.XMLName.Local != msg.XMLName.Local {
|
||||||
|
t.Fatalf("message tags do not match. Expected: %s, Actual: %s", msgConst.XMLName.Local, msg.XMLName.Local)
|
||||||
|
}
|
||||||
|
if msgConst.Body != msg.Body {
|
||||||
|
t.Fatalf("message bodies do not match. Expected: %s, Actual: %s", msgConst.Body, msg.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(msgConst.Attrs, msg.Attrs) {
|
||||||
|
t.Fatalf("attributes do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(msgConst.Error, msg.Error) {
|
||||||
|
t.Fatalf("attributes do not match")
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, ext := range msgConst.Extensions {
|
||||||
|
for _, strExt := range msg.Extensions {
|
||||||
|
if reflect.TypeOf(ext) == reflect.TypeOf(strExt) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("extensions do not match")
|
||||||
|
}
|
||||||
|
found = false
|
||||||
|
}
|
||||||
|
}
|
22
stanza/msg_html.go
Normal file
22
stanza/msg_html.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTML struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/xhtml-im html"`
|
||||||
|
Body HTMLBody
|
||||||
|
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTMLBody struct {
|
||||||
|
XMLName xml.Name `xml:"http://www.w3.org/1999/xhtml body"`
|
||||||
|
// InnerXML MUST be valid xhtml. We do not check if it is valid when generating the XMPP stanza.
|
||||||
|
InnerXML string `xml:",innerxml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/xhtml-im", Local: "html"}, HTML{})
|
||||||
|
}
|
44
stanza/msg_html_test.go
Normal file
44
stanza/msg_html_test.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTMLGen(t *testing.T) {
|
||||||
|
htmlBody := "<p>Hello <b>World</b></p>"
|
||||||
|
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||||
|
msg.Body = "Hello World"
|
||||||
|
body := stanza.HTMLBody{
|
||||||
|
InnerXML: htmlBody,
|
||||||
|
}
|
||||||
|
html := stanza.HTML{Body: body}
|
||||||
|
msg.Extensions = append(msg.Extensions, html)
|
||||||
|
|
||||||
|
result := msg.XMPPFormat()
|
||||||
|
str := `<message to="test@localhost"><body>Hello World</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>Hello <b>World</b></p></body></html></message>`
|
||||||
|
if result != str {
|
||||||
|
t.Errorf("incorrect serialize message:\n%s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedMessage := stanza.Message{}
|
||||||
|
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||||
|
t.Errorf("message HTML unmarshall error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedMessage.Body != msg.Body {
|
||||||
|
t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var h stanza.HTML
|
||||||
|
if ok := parsedMessage.Get(&h); !ok {
|
||||||
|
t.Error("could not extract HTML body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Body.InnerXML != htmlBody {
|
||||||
|
t.Errorf("could not extract html body: '%s'", h.Body.InnerXML)
|
||||||
|
}
|
||||||
|
}
|
21
stanza/msg_oob.go
Normal file
21
stanza/msg_oob.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Support for:
|
||||||
|
- XEP-0066 - Out of Band Data: https://xmpp.org/extensions/xep-0066.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
type OOB struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"jabber:x:oob x"`
|
||||||
|
URL string `xml:"url"`
|
||||||
|
Desc string `xml:"desc,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "jabber:x:oob", Local: "x"}, OOB{})
|
||||||
|
}
|
213
stanza/msg_pubsub_event.go
Normal file
213
stanza/msg_pubsub_event.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implementation of the http://jabber.org/protocol/pubsub#event namespace
|
||||||
|
type PubSubEvent struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#event event"`
|
||||||
|
MsgExtension
|
||||||
|
EventElement EventElement
|
||||||
|
//List ItemsEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/pubsub#event", Local: "event"}, PubSubEvent{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventElement interface {
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Collection
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
const PubSubCollectionEventName = "Collection"
|
||||||
|
|
||||||
|
type CollectionEvent struct {
|
||||||
|
AssocDisassoc AssocDisassoc
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CollectionEvent) Name() string {
|
||||||
|
return PubSubCollectionEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Associate/Disassociate
|
||||||
|
// *********************
|
||||||
|
type AssocDisassoc interface {
|
||||||
|
GetAssocDisassoc() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Associate
|
||||||
|
// *********************
|
||||||
|
const Assoc = "Associate"
|
||||||
|
|
||||||
|
type AssociateEvent struct {
|
||||||
|
XMLName xml.Name `xml:"associate"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssociateEvent) GetAssocDisassoc() string {
|
||||||
|
return Assoc
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Disassociate
|
||||||
|
// *********************
|
||||||
|
const Disassoc = "Disassociate"
|
||||||
|
|
||||||
|
type DisassociateEvent struct {
|
||||||
|
XMLName xml.Name `xml:"disassociate"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DisassociateEvent) GetAssocDisassoc() string {
|
||||||
|
return Disassoc
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Configuration
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
const PubSubConfigEventName = "Configuration"
|
||||||
|
|
||||||
|
type ConfigurationEvent struct {
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Form *Form
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConfigurationEvent) Name() string {
|
||||||
|
return PubSubConfigEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Delete
|
||||||
|
// *********************
|
||||||
|
const PubSubDeleteEventName = "Delete"
|
||||||
|
|
||||||
|
type DeleteEvent struct {
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Redirect *RedirectEvent `xml:"redirect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c DeleteEvent) Name() string {
|
||||||
|
return PubSubConfigEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Redirect
|
||||||
|
// *********************
|
||||||
|
type RedirectEvent struct {
|
||||||
|
URI string `xml:"uri,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// List
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
const PubSubItemsEventName = "List"
|
||||||
|
|
||||||
|
type ItemsEvent struct {
|
||||||
|
XMLName xml.Name `xml:"items"`
|
||||||
|
Items []ItemEvent `xml:"item,omitempty"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Retract *RetractEvent `xml:"retract"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemEvent struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
Id string `xml:"id,attr,omitempty"`
|
||||||
|
Publisher string `xml:"publisher,attr,omitempty"`
|
||||||
|
Any *Node `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ItemsEvent) Name() string {
|
||||||
|
return PubSubItemsEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// List
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
type RetractEvent struct {
|
||||||
|
XMLName xml.Name `xml:"retract"`
|
||||||
|
ID string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Purge
|
||||||
|
// *********************
|
||||||
|
const PubSubPurgeEventName = "Purge"
|
||||||
|
|
||||||
|
type PurgeEvent struct {
|
||||||
|
XMLName xml.Name `xml:"purge"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PurgeEvent) Name() string {
|
||||||
|
return PubSubPurgeEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Subscription
|
||||||
|
// *********************
|
||||||
|
const PubSubSubscriptionEventName = "Subscription"
|
||||||
|
|
||||||
|
type SubscriptionEvent struct {
|
||||||
|
SubStatus string `xml:"subscription,attr,omitempty"`
|
||||||
|
Expiry string `xml:"expiry,attr,omitempty"`
|
||||||
|
SubInfo `xml:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SubscriptionEvent) Name() string {
|
||||||
|
return PubSubSubscriptionEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
pse.XMLName = start.Name
|
||||||
|
// decode inner elements
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var ee EventElement
|
||||||
|
switch tt := t.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
switch tt.Name.Local {
|
||||||
|
case "collection":
|
||||||
|
ee = &CollectionEvent{}
|
||||||
|
case "configuration":
|
||||||
|
ee = &ConfigurationEvent{}
|
||||||
|
case "delete":
|
||||||
|
ee = &DeleteEvent{}
|
||||||
|
case "items":
|
||||||
|
ee = &ItemsEvent{}
|
||||||
|
case "purge":
|
||||||
|
ee = &PurgeEvent{}
|
||||||
|
case "subscription":
|
||||||
|
ee = &SubscriptionEvent{}
|
||||||
|
default:
|
||||||
|
ee = nil
|
||||||
|
}
|
||||||
|
// known child element found, decode it
|
||||||
|
if ee != nil {
|
||||||
|
err = d.DecodeElement(ee, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pse.EventElement = ee
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user