forked from jshiffer/go-xmpp
Compare commits
166 Commits
v0.1.1
..
7ec2b8b7de
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec2b8b7de | |||
| 43e817cebe | |||
| 66aade104d | |||
| 2271ce0aec | |||
| fda8e5cb42 | |||
| bd84bf7b04 | |||
| 44c76a8761 | |||
| 3e4f4a3a80 | |||
| d0cdb99fae | |||
| 127e75bc8b | |||
| e69bd697cb | |||
| 2f138678c0 | |||
| e015f92cdf | |||
| fe382e4805 | |||
| 7ec8e81ec3 | |||
| f3cf3c3b40 | |||
| 906d9d747d | |||
| 16b6a7bdba | |||
| a74ec7bb2d | |||
| ac40267866 | |||
| 1610c524f7 | |||
| 0fe2a76e77 | |||
| 325c112042 | |||
| 18cda4524c | |||
| f4550b5399 | |||
| f66ee47cd9 | |||
| 02db6f5ed6 | |||
| 62f9ce3246 | |||
| 6265286138 | |||
| ccac8addc9 | |||
| e44d1877bb | |||
| c7af92b53b | |||
| aeb80ddc4d | |||
| bacbdeb205 | |||
| 12d5633a9d | |||
| 0948d88dae | |||
| e3871c2deb | |||
| 7c0791141b | |||
| 54cdc20727 | |||
| 0e6327115f | |||
| e810b2faca | |||
| adbceb5dae | |||
| c84fc9afab | |||
| 7d83a73298 | |||
| 5197953ad4 | |||
| 089ebf9bad | |||
| 9df9a5b5f9 | |||
| 9aeb3722bf | |||
| 188e3f03c7 | |||
| d86062634d | |||
| 637503f492 | |||
| 6618fc47ca | |||
| 84b9ced4e9 | |||
| 88f429802e | |||
| 222c8f8fd0 | |||
| 5f7c3b14b0 | |||
| b5c8af17a7 | |||
| 02e423485e | |||
| c8c5371616 | |||
| 0c0c98633c | |||
| 861872c8db | |||
| 404638fb3d | |||
| 874e70e091 | |||
| 9c349bcc3f | |||
| 6c1f4b23f8 | |||
| a1c1069091 | |||
| e8c25dcffe | |||
| cc56ae0810 | |||
| 9dd92e1247 | |||
| 09fb80afad | |||
| 8b13d0ad77 | |||
| 70ac466680 | |||
| 37dcc8bfca | |||
| 58077b314a | |||
| 4e8e43b7ca | |||
| 050bbf66bd | |||
| 0d259f5448 | |||
| c0f9b41b3d | |||
| 0655f5913b | |||
| 0a3375a6ad | |||
| a60980a550 | |||
| 98a0431d5b | |||
| 7568e71728 | |||
| 846b8175da | |||
| 477ccf01f6 | |||
| 15ac96c029 | |||
| d03bc801da | |||
| 9fe31adf02 | |||
| 61f20ce1de | |||
| 08299587ec | |||
| 1ff5be0d01 | |||
| 7a8cf41551 | |||
| 1f559fafde | |||
| ebd519cbfe | |||
| ac5d015101 | |||
| aa9390a115 | |||
| c9bbe151b2 | |||
| f402673c8c | |||
| 748282a14a | |||
| 15989a19c3 | |||
| aa27e3ee45 | |||
| 91047d400c | |||
| 11887e6acb | |||
| f06f19e121 | |||
| 41fd432f88 | |||
| 0fd114068f | |||
| 9276abaad9 | |||
| 8a08b956bb | |||
| f467ba7632 | |||
| e71f933d7c | |||
| 8da045a9e5 | |||
| 8a80c8abe3 | |||
| bb0e2d84ea | |||
| b67dc40516 | |||
| fd49820bf5 | |||
| 64821d5df9 | |||
| cbdf478ba7 | |||
| 73e63850ab | |||
| ba140e5eb7 | |||
| 12d98ae2dc | |||
| c88c22763a | |||
| af110491a0 | |||
| c3ac597871 | |||
| 700abb7449 | |||
| 7c9260e5a0 | |||
| 5e57ac52f9 | |||
| 52f561e157 | |||
| 4a4cac6dfc | |||
| 26e35d5504 | |||
| 53846d1e34 | |||
| df87efc875 | |||
| 75752790eb | |||
| 52c3f1b710 | |||
| 1e9dc674d1 | |||
| 99516ec31f | |||
| df2ef04578 | |||
| d92790f748 | |||
| cb6591b513 | |||
| 1dd8c2eeac | |||
| a25b82d0a4 | |||
| e4e8b7448c | |||
| e7715f9f21 | |||
| 2ec48a2db6 | |||
| f8ce0ead60 | |||
| f482858a52 | |||
| 4201b13e32 | |||
| 0a0f20b95e | |||
| b6a67e2320 | |||
| 4ddb93ef9d | |||
| fc3904b3a3 | |||
| 514bb10a8e | |||
| 7f4668977b | |||
| 986d7a0046 | |||
| 2644d2a47e | |||
| c922a1691f | |||
| 19a383f5b4 | |||
| 17fe2a046a | |||
| 094f05fb75 | |||
| 7ff519ae8c | |||
| fca5966193 | |||
| c3beae4a4e | |||
| aaaea96dd8 | |||
| 73126a5d4b | |||
| 08d0d26b2d | |||
| d5ee92e89e | |||
| 76fbc3bb63 |
-36
@@ -1,36 +0,0 @@
|
|||||||
# 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/
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
script:
|
||||||
|
- go test
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# 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
@@ -1,139 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
FROM golang:1.12
|
|
||||||
WORKDIR /xmpp
|
|
||||||
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
|
|
||||||
COPY . ./
|
|
||||||
@@ -1,29 +1,27 @@
|
|||||||
BSD 3-Clause License
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
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 met:
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
* Redistributions of source code must retain the above copyright
|
||||||
list of conditions and the following disclaimer.
|
notice, this 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
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
this list of conditions and the following disclaimer in the documentation
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
and/or other materials provided with the distribution.
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
* Neither the name of the copyright holder nor the names of its
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
contributors may be used to endorse or promote products derived from
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
this software without specific prior written permission.
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|||||||
@@ -1,133 +1,6 @@
|
|||||||
# Fluux XMPP
|
go-xmpp
|
||||||
|
=======
|
||||||
|
|
||||||
[](https://app.codeship.com/projects/262399) [](https://godoc.org/gosrc.io/xmpp) [](https://goreportcard.com/report/fluux.io/xmpp) [](https://codecov.io/gh/FluuxIO/go-xmpp)
|
go xmpp library (original was written by russ cox )
|
||||||
|
|
||||||
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
[Documentation](https://godoc.org/github.com/mattn/go-xmpp)
|
||||||
|
|
||||||
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. For now, the library does not depend on any other library.
|
|
||||||
|
|
||||||
## 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",
|
|
||||||
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)
|
|
||||||
|
|
||||||
## 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).
|
|
||||||
|
|
||||||
## 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{
|
|
||||||
Address: "localhost:5222",
|
|
||||||
Jid: "test@localhost",
|
|
||||||
Password: "test",
|
|
||||||
StreamLogger: os.Stdout,
|
|
||||||
Insecure: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
router := xmpp.NewRouter()
|
|
||||||
router.HandleFunc("message", handleMessage)
|
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reference documentation
|
|
||||||
|
|
||||||
The code documentation is available on GoDoc: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
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]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"})
|
|
||||||
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 init() {
|
|
||||||
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# 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)).
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
opts := xmpp.ComponentOptions{
|
|
||||||
Domain: "service.localhost",
|
|
||||||
Secret: "mypass",
|
|
||||||
Address: "localhost:9999",
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
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 := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
|
||||||
|
|
||||||
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.PubSub)
|
|
||||||
if !ok {
|
|
||||||
// We only support pubsub delegation
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if pubsub.Publish.XMLName.Local == "publish" {
|
|
||||||
// Prepare pubsub IQ reply
|
|
||||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
|
|
||||||
payload := stanza.PubSub{
|
|
||||||
XMLName: xml.Name{
|
|
||||||
Space: "http://jabber.org/protocol/pubsub",
|
|
||||||
Local: "pubsub",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
iqResp.Payload = &payload
|
|
||||||
// Wrap the reply in delegation 'forward'
|
|
||||||
iqForward := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module gosrc.io/xmpp/_examples
|
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/processone/mpg123 v1.0.0
|
|
||||||
github.com/processone/soundcloud v1.0.0
|
|
||||||
gosrc.io/xmpp v0.1.1
|
|
||||||
)
|
|
||||||
|
|
||||||
replace gosrc.io/xmpp => ./../
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
|
||||||
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
|
||||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 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".
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
opts := xmpp.ComponentOptions{
|
|
||||||
Domain: "service2.localhost",
|
|
||||||
Secret: "mypass",
|
|
||||||
Address: "localhost:8888",
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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 != "get" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
|
||||||
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 != "get" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
discoItems, ok := iq.Payload.(*stanza.DiscoItems)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
|
||||||
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 := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
|
||||||
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
|
|
||||||
_ = c.Send(iqResp)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
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{
|
|
||||||
Address: "localhost:5222",
|
|
||||||
Jid: "test@localhost",
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
|
|
||||||
// (using templates ?)
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// Can be launched with:
|
|
||||||
// ./xmpp_jukebox -jid=test@localhost/jukebox -password=test -address=localhost:5222
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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{
|
|
||||||
Address: *address,
|
|
||||||
Jid: *jid,
|
|
||||||
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("message").
|
|
||||||
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
|
||||||
handleIQ(s, p, player)
|
|
||||||
})
|
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cm := xmpp.NewStreamManager(client, nil)
|
|
||||||
log.Fatal(cm.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
tune := stanza.Tune{Artist: artist, Title: title}
|
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
|
|
||||||
payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
|
|
||||||
iq.Payload = &payload
|
|
||||||
_ = s.Send(iq)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
|
|
||||||
// TODO: Implement other type of SASL Authentication
|
|
||||||
havePlain := false
|
|
||||||
for _, m := range f.Mechanisms.Mechanism {
|
|
||||||
if m == "PLAIN" {
|
|
||||||
havePlain = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !havePlain {
|
|
||||||
err := fmt.Errorf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism)
|
|
||||||
return NewConnError(err, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return authPlain(socket, decoder, user, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plain authentication: send base64-encoded \x00 user \x00 password
|
|
||||||
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
|
|
||||||
raw := "\x00" + user + "\x00" + password
|
|
||||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
|
||||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
|
||||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-148
@@ -1,148 +0,0 @@
|
|||||||
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, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); 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 {
|
|
||||||
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// EventManager
|
|
||||||
|
|
||||||
// ConnState represents the current connection state.
|
|
||||||
type ConnState = uint8
|
|
||||||
|
|
||||||
// This is a the list of events happening on the connection that the
|
|
||||||
// client can be notified about.
|
|
||||||
const (
|
|
||||||
StateDisconnected ConnState = iota
|
|
||||||
StateConnected
|
|
||||||
StateSessionEstablished
|
|
||||||
StateStreamError
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 ConnState
|
|
||||||
Description string
|
|
||||||
StreamError string
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventHandler is use to pass events about state of the connection to
|
|
||||||
// client implementation.
|
|
||||||
type EventHandler func(Event)
|
|
||||||
|
|
||||||
type EventManager struct {
|
|
||||||
// Store current state
|
|
||||||
CurrentState ConnState
|
|
||||||
|
|
||||||
// Callback used to propagate connection state changes
|
|
||||||
Handler EventHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (em EventManager) updateState(state ConnState) {
|
|
||||||
em.CurrentState = state
|
|
||||||
if em.Handler != nil {
|
|
||||||
em.Handler(Event{State: em.CurrentState})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (em EventManager) streamError(error, desc string) {
|
|
||||||
em.CurrentState = StateStreamError
|
|
||||||
if em.Handler != nil {
|
|
||||||
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// TCP level connection / can be replaced by a TLS session after starttls
|
|
||||||
conn net.Conn
|
|
||||||
// Router is used to dispatch packets
|
|
||||||
router *Router
|
|
||||||
// Track and broadcast connection state
|
|
||||||
EventManager
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
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 domainpart of the JID.
|
|
||||||
// Default the port to 5222.
|
|
||||||
func NewClient(config Config, r *Router) (c *Client, err error) {
|
|
||||||
// Parse JID
|
|
||||||
if config.parsedJid, err = NewJid(config.Jid); err != nil {
|
|
||||||
err = errors.New("missing jid")
|
|
||||||
return nil, NewConnError(err, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Password == "" {
|
|
||||||
err = errors.New("missing password")
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.Address = ensurePort(config.Address, 5222)
|
|
||||||
|
|
||||||
c = new(Client)
|
|
||||||
c.config = config
|
|
||||||
c.router = r
|
|
||||||
|
|
||||||
if c.config.ConnectTimeout == 0 {
|
|
||||||
c.config.ConnectTimeout = 15 // 15 second as default
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
|
||||||
func (c *Client) Connect() error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
c.conn, err = net.DialTimeout("tcp", c.config.Address, time.Duration(c.config.ConnectTimeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.updateState(StateConnected)
|
|
||||||
|
|
||||||
// Client is ok, we now open XMPP session
|
|
||||||
if c.conn, c.Session, err = NewSession(c.conn, c.config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.updateState(StateSessionEstablished)
|
|
||||||
|
|
||||||
// We're connected and can now receive and send messages.
|
|
||||||
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
|
||||||
// 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 ?
|
|
||||||
fmt.Fprintf(c.Session.streamLogger, "<presence/>")
|
|
||||||
|
|
||||||
// Start the keepalive go routine
|
|
||||||
keepaliveQuit := make(chan struct{})
|
|
||||||
go keepalive(c.conn, keepaliveQuit)
|
|
||||||
// Start the receiver go routine
|
|
||||||
go c.recv(keepaliveQuit)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Disconnect() {
|
|
||||||
_ = c.SendRaw("</stream:stream>")
|
|
||||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
|
||||||
_ = c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
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.conn
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.sendWithLogger(string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.conn
|
|
||||||
if conn == nil {
|
|
||||||
return errors.New("client is not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.sendWithLogger(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) sendWithLogger(packet string) error {
|
|
||||||
var err error
|
|
||||||
_, err = fmt.Fprintf(c.Session.streamLogger, packet)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Go routines
|
|
||||||
|
|
||||||
// Loop: Receive data from server
|
|
||||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
|
||||||
for {
|
|
||||||
val, err := stanza.NextPacket(c.Session.decoder)
|
|
||||||
if err != nil {
|
|
||||||
close(keepaliveQuit)
|
|
||||||
c.updateState(StateDisconnected)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle stream errors
|
|
||||||
switch packet := val.(type) {
|
|
||||||
case stanza.StreamError:
|
|
||||||
c.router.route(c, val)
|
|
||||||
close(keepaliveQuit)
|
|
||||||
c.streamError(packet.Error.Local, packet.Text)
|
|
||||||
return errors.New("stream error: " + packet.Error.Local)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(conn net.Conn, quit <-chan struct{}) {
|
|
||||||
// TODO: Make keepalive interval configurable
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
if n, err := fmt.Fprintf(conn, "\n"); err != nil || n != 1 {
|
|
||||||
// When keep alive fails, we force close the connection. In all cases, the recv will also fail.
|
|
||||||
ticker.Stop()
|
|
||||||
_ = conn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-quit:
|
|
||||||
ticker.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-281
@@ -1,281 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"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"
|
|
||||||
|
|
||||||
defaultTimeout = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestClient_Connect(t *testing.T) {
|
|
||||||
// Setup Mock server
|
|
||||||
mock := ServerMock{}
|
|
||||||
mock.Start(t, testXMPPAddress, handlerConnectSuccess)
|
|
||||||
|
|
||||||
// Test / Check result
|
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
|
|
||||||
|
|
||||||
var client *Client
|
|
||||||
var err error
|
|
||||||
router := NewRouter()
|
|
||||||
if client, err = NewClient(config, router); 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, handlerAbortTLS)
|
|
||||||
|
|
||||||
// Test / Check result
|
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
|
|
||||||
|
|
||||||
var client *Client
|
|
||||||
var err error
|
|
||||||
router := NewRouter()
|
|
||||||
if client, err = NewClient(config, router); 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, handlerAbortTLS)
|
|
||||||
|
|
||||||
// Test / Check result
|
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
|
|
||||||
|
|
||||||
var client *Client
|
|
||||||
var err error
|
|
||||||
router := NewRouter()
|
|
||||||
if client, err = NewClient(config, router); 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, handlerConnectWithSession)
|
|
||||||
|
|
||||||
// Test / Check result
|
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
|
|
||||||
|
|
||||||
var client *Client
|
|
||||||
var err error
|
|
||||||
router := NewRouter()
|
|
||||||
if client, err = NewClient(config, router); 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// Basic XMPP Server Mock Handlers.
|
|
||||||
|
|
||||||
const serverStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' id='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
|
||||||
|
|
||||||
// Test connection with a basic straightforward workflow
|
|
||||||
func handlerConnectSuccess(t *testing.T, c net.Conn) {
|
|
||||||
decoder := xml.NewDecoder(c)
|
|
||||||
checkOpenStream(t, c, decoder)
|
|
||||||
|
|
||||||
sendStreamFeatures(t, c, decoder) // Send initial features
|
|
||||||
readAuth(t, decoder)
|
|
||||||
fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
|
|
||||||
|
|
||||||
checkOpenStream(t, c, decoder) // Reset stream
|
|
||||||
sendBindFeature(t, c, decoder) // Send post auth features
|
|
||||||
bind(t, c, decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect client will abort on TLS
|
|
||||||
func handlerAbortTLS(t *testing.T, c net.Conn) {
|
|
||||||
decoder := xml.NewDecoder(c)
|
|
||||||
checkOpenStream(t, c, decoder)
|
|
||||||
sendStreamFeatures(t, c, decoder) // Send initial features
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test connection with mandatory session (RFC-3921)
|
|
||||||
func handlerConnectWithSession(t *testing.T, c net.Conn) {
|
|
||||||
decoder := xml.NewDecoder(c)
|
|
||||||
checkOpenStream(t, c, decoder)
|
|
||||||
|
|
||||||
sendStreamFeatures(t, c, decoder) // Send initial features
|
|
||||||
readAuth(t, decoder)
|
|
||||||
fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
|
|
||||||
|
|
||||||
checkOpenStream(t, c, decoder) // Reset stream
|
|
||||||
sendRFC3921Feature(t, c, decoder) // Send post auth features
|
|
||||||
bind(t, c, decoder)
|
|
||||||
session(t, c, decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
|
||||||
c.SetDeadline(time.Now().Add(defaultTimeout))
|
|
||||||
defer c.SetDeadline(time.Time{})
|
|
||||||
|
|
||||||
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
|
|
||||||
var token xml.Token
|
|
||||||
token, err := 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(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
|
|
||||||
t.Errorf("cannot write server stream open: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
|
||||||
// This is a basic server, supporting only 1 stream feature: SASL Plain Auth
|
|
||||||
features := `<stream:features>
|
|
||||||
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
|
||||||
<mechanism>PLAIN</mechanism>
|
|
||||||
</mechanisms>
|
|
||||||
</stream:features>`
|
|
||||||
if _, err := fmt.Fprintln(c, features); err != nil {
|
|
||||||
t.Errorf("cannot send stream feature: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO return err in case of error reading the auth params
|
|
||||||
func readAuth(t *testing.T, decoder *xml.Decoder) string {
|
|
||||||
se, err := stanza.NextStart(decoder)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read auth: %s", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var nv interface{}
|
|
||||||
nv = &stanza.SASLAuth{}
|
|
||||||
// Decode element into pointer storage
|
|
||||||
if err = decoder.DecodeElement(nv, &se); err != nil {
|
|
||||||
t.Errorf("cannot decode auth: %s", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := nv.(type) {
|
|
||||||
case *stanza.SASLAuth:
|
|
||||||
return v.Value
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
|
||||||
// This is a basic server, supporting only 1 stream feature after auth: resource binding
|
|
||||||
features := `<stream:features>
|
|
||||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
|
||||||
</stream:features>`
|
|
||||||
if _, err := fmt.Fprintln(c, features); err != nil {
|
|
||||||
t.Errorf("cannot send stream feature: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
|
||||||
// This is a basic server, supporting only 2 features after auth: resource & session binding
|
|
||||||
features := `<stream:features>
|
|
||||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
|
||||||
<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
|
|
||||||
</stream:features>`
|
|
||||||
if _, err := fmt.Fprintln(c, features); err != nil {
|
|
||||||
t.Errorf("cannot send stream feature: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
|
||||||
se, err := stanza.NextStart(decoder)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read bind: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iq := &stanza.IQ{}
|
|
||||||
// Decode element into pointer storage
|
|
||||||
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
|
||||||
t.Errorf("cannot decode bind iq: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Check all elements
|
|
||||||
switch iq.Payload.(type) {
|
|
||||||
case *stanza.Bind:
|
|
||||||
result := `<iq id='%s' type='result'>
|
|
||||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
|
|
||||||
<jid>%s</jid>
|
|
||||||
</bind>
|
|
||||||
</iq>`
|
|
||||||
fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func session(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
|
||||||
se, err := stanza.NextStart(decoder)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read session: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iq := &stanza.IQ{}
|
|
||||||
// Decode element into pointer storage
|
|
||||||
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
|
||||||
t.Errorf("cannot decode session iq: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch iq.Payload.(type) {
|
|
||||||
case *stanza.StreamSession:
|
|
||||||
result := `<iq id='%s' type='result'/>`
|
|
||||||
fmt.Fprintf(c, result, iq.Id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
module gosrc.io/xmpp/cmd
|
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
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.4.0
|
|
||||||
gosrc.io/xmpp v0.1.1
|
|
||||||
)
|
|
||||||
|
|
||||||
replace gosrc.io/xmpp => ./../
|
|
||||||
-159
@@ -1,159 +0,0 @@
|
|||||||
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/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/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/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.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/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-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/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/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/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/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/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/konsorten/go-windows-terminal-sequences v1.0.1/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/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/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/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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
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.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
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/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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
|
||||||
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.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-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/net v0.0.0-20180826012351-8a410e7b638d/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/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/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-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/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/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/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/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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
|
||||||
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=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# sendXMPP
|
|
||||||
|
|
||||||
sendxmpp is a tool to send messages from command-line.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install `sendxmpp` in your Go path:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ go get -u gosrc.io/xmpp/cmd/sendxmpp
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
$ sendxmpp --help
|
|
||||||
Usage:
|
|
||||||
sendxmpp <recipient,> [message] [flags]
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
sendxmpp to@chat.sum7.eu "Hello World!"
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
--addr string host[:port]
|
|
||||||
--config string config file (default is ~/.config/fluxxmpp.yml)
|
|
||||||
-h, --help help for sendxmpp
|
|
||||||
--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
|
|
||||||
|
|
||||||
Message from arguments:
|
|
||||||
```bash
|
|
||||||
$ sendxmpp 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 | sendxmpp 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
|
|
||||||
$ sendxmpp 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 | sendxmpp 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 `fluxxmpp` with you favorite file extenion (e.g. `toml`, `yml`).
|
|
||||||
|
|
||||||
e.g. ~/.config/fluxxmpp.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';
|
|
||||||
|
|
||||||
sendxmpp 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
|
|
||||||
sendxmpp to@example.org "Hello World!" --jid bot@example.org --password secret --addr example.com:5222;
|
|
||||||
```
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
## 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 ?)
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"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 cmd = &cobra.Command{
|
|
||||||
Use: "sendxmpp <recipient,> [message]",
|
|
||||||
Example: `sendxmpp 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{
|
|
||||||
Jid: viper.GetString("jid"),
|
|
||||||
Address: viper.GetString("addr"),
|
|
||||||
Password: viper.GetString("password"),
|
|
||||||
}, xmpp.NewRouter())
|
|
||||||
|
|
||||||
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 []*xmpp.Jid
|
|
||||||
|
|
||||||
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
log.Info("client connected")
|
|
||||||
|
|
||||||
if isMUCRecipient {
|
|
||||||
for _, muc := range receiver {
|
|
||||||
jid, err := xmpp.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() {
|
|
||||||
cobra.OnInitialize(initConfig)
|
|
||||||
cmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ~/.config/fluxxmpp.yml)")
|
|
||||||
|
|
||||||
cmd.Flags().StringP("jid", "", "", "using jid (required)")
|
|
||||||
viper.BindPFlag("jid", cmd.Flags().Lookup("jid"))
|
|
||||||
|
|
||||||
cmd.Flags().StringP("password", "", "", "using password for your jid (required)")
|
|
||||||
viper.BindPFlag("password", cmd.Flags().Lookup("password"))
|
|
||||||
|
|
||||||
cmd.Flags().StringP("addr", "", "", "host[:port]")
|
|
||||||
viper.BindPFlag("addr", cmd.Flags().Lookup("addr"))
|
|
||||||
|
|
||||||
cmd.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 initConfig() {
|
|
||||||
if configFile != "" {
|
|
||||||
viper.SetConfigFile(configFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
viper.SetConfigName("fluxxmpp")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
sendxmpp is a command-line tool to send to send XMPP messages to users
|
|
||||||
|
|
||||||
*/
|
|
||||||
package main
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/bdlm/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.AddHook(&hook{})
|
|
||||||
if err := cmd.Execute(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/bdlm/log"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func joinMUC(c xmpp.Sender, toJID *xmpp.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 []*xmpp.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# XMPP Check
|
|
||||||
|
|
||||||
XMPP check is a tool to check TLS certificate on a remote server.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install `xmpp-check` in your Go path:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ go get -u gosrc.io/xmpp/cmd/xmpp-check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
$ xmpp-check --help
|
|
||||||
Usage:
|
|
||||||
xmpp-check <host[:port]> [flags]
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
xmpp-check chat.sum7.eu:5222 --domain meckerspace.de
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
-d, --domain string domain if host handle multiple domains
|
|
||||||
-h, --help help for xmpp-check
|
|
||||||
```
|
|
||||||
|
|
||||||
If you server is on standard port and XMPP domains matches the hostname you can simply use:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ xmpp-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:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ xmpp-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.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
- Use a config file to define the checks to perform as client on an XMPP server.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
xmpp-check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires.
|
|
||||||
|
|
||||||
*/
|
|
||||||
package main
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/bdlm/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.AddHook(&hook{})
|
|
||||||
cmd.Execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
var domain = ""
|
|
||||||
var cmd = &cobra.Command{
|
|
||||||
Use: "xmpp-check <host[:port]>",
|
|
||||||
Example: "xmpp-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() {
|
|
||||||
cmd.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")
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
comment: off
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
build:
|
|
||||||
build:
|
|
||||||
image: fluux/build
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
encrypted_env_file: codeship.env.encrypted
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
- type: serial
|
|
||||||
steps:
|
|
||||||
- name: test
|
|
||||||
service: build
|
|
||||||
command: ./test.sh
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
yVKgVFeKW6SSnC/KgLYpfYtTcqqTke1gOIW5GUiVvRijnhweOJiYKFPmwPjpt1FVrg4WVELQUNbxn3lmfyHVVF7r
|
|
||||||
-195
@@ -1,195 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
|
|
||||||
|
|
||||||
type ComponentOptions struct {
|
|
||||||
// =================================
|
|
||||||
// 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
|
|
||||||
// Address is the XMPP Host and port to connect to. Host is of
|
|
||||||
// the form 'serverhost:port' i.e "localhost:8888"
|
|
||||||
Address 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
|
|
||||||
|
|
||||||
// TCP level connection
|
|
||||||
conn net.Conn
|
|
||||||
|
|
||||||
// read / write
|
|
||||||
socketProxy io.ReadWriter // TODO
|
|
||||||
decoder *xml.Decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
|
|
||||||
c := Component{ComponentOptions: opts, router: r}
|
|
||||||
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 {
|
|
||||||
var conn net.Conn
|
|
||||||
var err error
|
|
||||||
if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.conn = conn
|
|
||||||
|
|
||||||
// 1. Send stream open tag
|
|
||||||
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
|
|
||||||
return errors.New("cannot send stream open " + err.Error())
|
|
||||||
}
|
|
||||||
c.decoder = xml.NewDecoder(conn)
|
|
||||||
|
|
||||||
// 2. Initialize xml decoder and extract streamID from reply
|
|
||||||
streamId, err := stanza.InitStream(c.decoder)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("cannot init decoder " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Authentication
|
|
||||||
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
|
|
||||||
return errors.New("cannot send handshake " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Check server response for authentication
|
|
||||||
val, err := stanza.NextPacket(c.decoder)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := val.(type) {
|
|
||||||
case stanza.StreamError:
|
|
||||||
return errors.New("handshake failed " + v.Error.Local)
|
|
||||||
case stanza.Handshake:
|
|
||||||
// Start the receiver go routine
|
|
||||||
go c.recv()
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errors.New("expecting handshake result, got " + v.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Component) Disconnect() {
|
|
||||||
_ = c.SendRaw("</stream:stream>")
|
|
||||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
|
||||||
_ = c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Component) SetHandler(handler EventHandler) {
|
|
||||||
c.Handler = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver Go routine receiver
|
|
||||||
func (c *Component) recv() (err error) {
|
|
||||||
for {
|
|
||||||
val, err := stanza.NextPacket(c.decoder)
|
|
||||||
if err != nil {
|
|
||||||
c.updateState(StateDisconnected)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle stream errors
|
|
||||||
switch p := val.(type) {
|
|
||||||
case stanza.StreamError:
|
|
||||||
c.router.route(c, val)
|
|
||||||
c.streamError(p.Error.Local, p.Text)
|
|
||||||
return errors.New("stream error: " + p.Error.Local)
|
|
||||||
}
|
|
||||||
c.router.route(c, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send marshalls XMPP stanza and sends it to the server.
|
|
||||||
func (c *Component) Send(packet stanza.Packet) error {
|
|
||||||
conn := c.conn
|
|
||||||
if conn == 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 := fmt.Fprintf(conn, string(data)); err != nil {
|
|
||||||
return errors.New("cannot send packet " + err.Error())
|
|
||||||
}
|
|
||||||
return 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 {
|
|
||||||
conn := c.conn
|
|
||||||
if conn == nil {
|
|
||||||
return errors.New("component is not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
_, err = fmt.Fprintf(c.conn, 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
|
|
||||||
*/
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateHandshake(t *testing.T) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Address string
|
|
||||||
Jid string
|
|
||||||
parsedJid *Jid // For easier manipulation
|
|
||||||
Password string
|
|
||||||
StreamLogger *os.File // Used for debugging
|
|
||||||
Lang string // TODO: should default to 'en'
|
|
||||||
ConnectTimeout int // Client timeout in seconds. Default to 15
|
|
||||||
// tls.Config must not be modified after having been passed to NewClient. The
|
|
||||||
// Client connect method may override the tls.Config.ServerName if it was not set.
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
// 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
|
|
||||||
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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 }
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
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
|
|
||||||
connections.
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
module gosrc.io/xmpp
|
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/google/go-cmp v0.3.0
|
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
|
||||||
)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
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=
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return j.Node + "@" + j.Domain + "/" + j.Resource
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jid) Bare() string {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
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) {
|
|
||||||
jid := "test@domain.com/my resource"
|
|
||||||
parsedJid, err := NewJid(jid)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not parse jid: %v", err)
|
|
||||||
}
|
|
||||||
fullJid := parsedJid.Full()
|
|
||||||
if fullJid != jid {
|
|
||||||
t.Errorf("incorrect full jid: %s", fullJid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBare(t *testing.T) {
|
|
||||||
jid := "test@domain.com"
|
|
||||||
fullJid := jid + "/my resource"
|
|
||||||
parsedJid, err := NewJid(fullJid)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not parse jid: %v", err)
|
|
||||||
}
|
|
||||||
bareJid := parsedJid.Bare()
|
|
||||||
if bareJid != jid {
|
|
||||||
t.Errorf("incorrect bare jid: %s", bareJid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
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 IPV$ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type params struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRouter returns a new router instance.
|
|
||||||
func NewRouter() *Router {
|
|
||||||
return &Router{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
|
|
||||||
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 iq, ok := p.(stanza.IQ); ok {
|
|
||||||
if iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet {
|
|
||||||
iqNotImplemented(s, iq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 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
|
|
||||||
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
|
|
||||||
}
|
|
||||||
-252
@@ -1,252 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Test route & matchers
|
|
||||||
|
|
||||||
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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
|
||||||
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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
|
||||||
// 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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
|
||||||
// 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 := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
|
||||||
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("get").
|
|
||||||
HandlerFunc(func(s Sender, p stanza.Packet) {
|
|
||||||
_ = s.SendRaw(successFlag)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Data set
|
|
||||||
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
|
||||||
getVersionIq.Payload = &stanza.Version{
|
|
||||||
XMLName: xml.Name{
|
|
||||||
Space: "jabber:iq:version",
|
|
||||||
Local: "query",
|
|
||||||
}}
|
|
||||||
|
|
||||||
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
|
|
||||||
setVersionIq.Payload = &stanza.Version{
|
|
||||||
XMLName: xml.Name{
|
|
||||||
Space: "jabber:iq:version",
|
|
||||||
Local: "query",
|
|
||||||
}}
|
|
||||||
|
|
||||||
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
|
||||||
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 := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
|
||||||
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
|
|
||||||
|
|
||||||
var successFlag = "matched"
|
|
||||||
|
|
||||||
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) 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-207
@@ -1,207 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
// Session info
|
|
||||||
BindJid string // Jabber ID as provided by XMPP server
|
|
||||||
StreamId string
|
|
||||||
Features stanza.StreamFeatures
|
|
||||||
TlsEnabled bool
|
|
||||||
lastPacketId int
|
|
||||||
|
|
||||||
// read / write
|
|
||||||
streamLogger io.ReadWriter
|
|
||||||
decoder *xml.Decoder
|
|
||||||
|
|
||||||
// error management
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
|
|
||||||
s := new(Session)
|
|
||||||
s.init(conn, o)
|
|
||||||
|
|
||||||
// starttls
|
|
||||||
var tlsConn net.Conn
|
|
||||||
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o)
|
|
||||||
|
|
||||||
if !s.TlsEnabled && !o.Insecure {
|
|
||||||
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
|
||||||
return nil, nil, NewConnError(err, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.TlsEnabled {
|
|
||||||
s.reset(conn, tlsConn, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
// auth
|
|
||||||
s.auth(o)
|
|
||||||
s.reset(tlsConn, tlsConn, o)
|
|
||||||
|
|
||||||
// bind resource and 'start' XMPP session
|
|
||||||
s.bind(o)
|
|
||||||
s.rfc3921Session(o)
|
|
||||||
|
|
||||||
return tlsConn, s, s.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) PacketId() string {
|
|
||||||
s.lastPacketId++
|
|
||||||
return fmt.Sprintf("%x", s.lastPacketId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) init(conn net.Conn, o Config) {
|
|
||||||
s.setStreamLogger(nil, conn, o)
|
|
||||||
s.Features = s.open(o.parsedJid.Domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) reset(conn net.Conn, newConn net.Conn, o Config) {
|
|
||||||
if s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.setStreamLogger(conn, newConn, o)
|
|
||||||
s.Features = s.open(o.parsedJid.Domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) setStreamLogger(conn net.Conn, newConn net.Conn, o Config) {
|
|
||||||
if newConn != conn {
|
|
||||||
s.streamLogger = newStreamLogger(newConn, o.StreamLogger)
|
|
||||||
}
|
|
||||||
s.decoder = xml.NewDecoder(s.streamLogger)
|
|
||||||
s.decoder.CharsetReader = o.CharsetReader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
|
||||||
// Send stream open tag
|
|
||||||
if _, s.err = fmt.Fprintf(s.streamLogger, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set xml decoder and extract streamID from reply
|
|
||||||
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
|
|
||||||
if s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract stream features
|
|
||||||
if s.err = s.decoder.Decode(&f); s.err != nil {
|
|
||||||
s.err = errors.New("stream open decode features: " + s.err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) net.Conn {
|
|
||||||
if s.err != nil {
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := s.Features.DoesStartTLS(); ok {
|
|
||||||
fmt.Fprintf(s.streamLogger, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
|
||||||
|
|
||||||
var k stanza.TLSProceed
|
|
||||||
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
|
|
||||||
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.TLSConfig == nil {
|
|
||||||
o.TLSConfig = &tls.Config{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.TLSConfig.ServerName == "" {
|
|
||||||
o.TLSConfig.ServerName = domain
|
|
||||||
}
|
|
||||||
tlsConn := tls.Client(conn, o.TLSConfig)
|
|
||||||
// We convert existing connection to TLS
|
|
||||||
if s.err = tlsConn.Handshake(); s.err != nil {
|
|
||||||
return tlsConn
|
|
||||||
}
|
|
||||||
|
|
||||||
if !o.TLSConfig.InsecureSkipVerify {
|
|
||||||
// We check that cert matches hostname
|
|
||||||
s.err = tlsConn.VerifyHostname(domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.err == nil {
|
|
||||||
s.TlsEnabled = true
|
|
||||||
}
|
|
||||||
return tlsConn
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we do not allow cleartext connections, make it explicit that server do not support starttls
|
|
||||||
if !o.Insecure {
|
|
||||||
s.err = errors.New("XMPP server does not advertise support for starttls")
|
|
||||||
}
|
|
||||||
|
|
||||||
// starttls is not supported => we do not upgrade the connection:
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) auth(o Config) {
|
|
||||||
if s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
if resource != "" {
|
|
||||||
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
|
||||||
s.PacketId(), stanza.NSBind, resource)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
|
||||||
}
|
|
||||||
|
|
||||||
var iq stanza.IQ
|
|
||||||
if s.err = s.decoder.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(o Config) {
|
|
||||||
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() {
|
|
||||||
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
|
|
||||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
|
||||||
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# 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{})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Namespace() string {
|
|
||||||
return d.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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.(*PubSub); 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
/*
|
|
||||||
XMPP stanza package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
|
||||||
*/
|
|
||||||
package stanza
|
|
||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 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"}
|
|
||||||
if elt.XMLName == textName {
|
|
||||||
x.Text = string(elt.Content)
|
|
||||||
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
|
|
||||||
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}
|
|
||||||
e.EncodeToken(xml.StartElement{Name: reason})
|
|
||||||
e.EncodeToken(xml.EndElement{Name: reason})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text
|
|
||||||
if x.Text != "" {
|
|
||||||
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
|
||||||
e.EncodeToken(xml.StartElement{Name: text})
|
|
||||||
e.EncodeToken(xml.CharData(x.Text))
|
|
||||||
e.EncodeToken(xml.EndElement{Name: text})
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ControlSet struct {
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
|
|
||||||
Fields []ControlField `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ControlSet) Namespace() string {
|
|
||||||
return c.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Registry init
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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("Paylod is not an iot control set: %v", cs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-130
@@ -1,130 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
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 generique structure
|
|
||||||
Any *Node `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IQPayload interface {
|
|
||||||
Namespace() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIQ(a Attrs) IQ {
|
|
||||||
// TODO generate IQ ID if not set
|
|
||||||
// TODO ensure that type is set, as it is required
|
|
||||||
return IQ{
|
|
||||||
XMLName: xml.Name{Local: "iq"},
|
|
||||||
Attrs: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
fmt.Println(err)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Disco Info
|
|
||||||
|
|
||||||
const (
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DiscoInfo) Namespace() string {
|
|
||||||
return d.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------
|
|
||||||
// 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DiscoItems) Namespace() string {
|
|
||||||
return d.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------
|
|
||||||
// Builder helpers
|
|
||||||
|
|
||||||
// DiscoItems builds a default DiscoItems payload
|
|
||||||
func (iq *IQ) DiscoItems() *DiscoItems {
|
|
||||||
d := DiscoItems{
|
|
||||||
XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", 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{NSDiscoInfo, "query"}, DiscoInfo{})
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package stanza_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test DiscoInfo Builder with several features
|
|
||||||
func TestDiscoInfo_Builder(t *testing.T) {
|
|
||||||
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")
|
|
||||||
|
|
||||||
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 := stanza.NewIQ(stanza.Attrs{Type: "result", From: "catalog.shakespeare.lit",
|
|
||||||
To: "romeo@montague.net/orchard", Id: "items-2"})
|
|
||||||
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("Items 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
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 TestGenerateIq(t *testing.T) {
|
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
|
||||||
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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Version) Namespace() string {
|
|
||||||
return v.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------
|
|
||||||
// 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{"jabber:iq:version", "query"}, Version{})
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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 := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
|
|
||||||
To: "juliet@capulet.com/balcony", Id: "version_1"})
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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{NSMsgChatMarkers, "markable"}, Markable{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "received"}, MarkReceived{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "displayed"}, MarkDisplayed{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "acknowledged"}, MarkAcknowledged{})
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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{NSMsgChatStateNotifications, "active"}, StateActive{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "composing"}, StateComposing{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "gone"}, StateGone{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "inactive"}, StateInactive{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "paused"}, StatePaused{})
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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{"http://jabber.org/protocol/xhtml-im", "html"}, HTML{})
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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{"jabber:x:oob", "x"}, OOB{})
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Support for:
|
|
||||||
- XEP-0184 - Message Delivery Receipts: https://xmpp.org/extensions/xep-0184.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NSMsgReceipts = "urn:xmpp:receipts"
|
|
||||||
|
|
||||||
// Used on outgoing message, to tell the recipient that you are requesting a message receipt / ack.
|
|
||||||
type ReceiptRequest struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:receipts request"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReceiptReceived struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:receipts received"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "request"}, ReceiptRequest{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "received"}, ReceiptReceived{})
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package stanza_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDecodeRequest(t *testing.T) {
|
|
||||||
str := `<message
|
|
||||||
from='northumberland@shakespeare.lit/westminster'
|
|
||||||
id='richard2-4.1.247'
|
|
||||||
to='kingrichard@royalty.england.lit/throne'>
|
|
||||||
<body>My lord, dispatch; read o'er these articles.</body>
|
|
||||||
<request xmlns='urn:xmpp:receipts'/>
|
|
||||||
</message>`
|
|
||||||
parsedMessage := stanza.Message{}
|
|
||||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
|
||||||
t.Errorf("message receipt unmarshall error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedMessage.Body != "My lord, dispatch; read o'er these articles." {
|
|
||||||
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parsedMessage.Extensions) < 1 {
|
|
||||||
t.Errorf("no extension found on parsed message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ext := parsedMessage.Extensions[0].(type) {
|
|
||||||
case *stanza.ReceiptRequest:
|
|
||||||
if ext.XMLName.Local != "request" {
|
|
||||||
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Errorf("could not find receipts extension")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import "encoding/xml"
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Generic / unknown content
|
|
||||||
|
|
||||||
// Node is a generic structure to represent XML data. It is used to parse
|
|
||||||
// unreferenced or custom stanza payload.
|
|
||||||
type Node struct {
|
|
||||||
XMLName xml.Name
|
|
||||||
Attrs []xml.Attr `xml:"-"`
|
|
||||||
Content string `xml:",innerxml"`
|
|
||||||
Nodes []Node `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) Namespace() string {
|
|
||||||
return n.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attr represents generic XML attributes, as used on the generic XML Node
|
|
||||||
// representation.
|
|
||||||
type Attr struct {
|
|
||||||
K string
|
|
||||||
V string
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
|
||||||
// transform generic XML content into hierarchical Node structure.
|
|
||||||
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
|
|
||||||
for _, attr := range start.Attr {
|
|
||||||
// Do not repeat xmlns, it is already in XMLName
|
|
||||||
if attr.Name.Local != "xmlns" {
|
|
||||||
n.Attrs = append(n.Attrs, attr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type node Node
|
|
||||||
return d.DecodeElement((*node)(n), &start)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
|
|
||||||
// Node structure to XML.
|
|
||||||
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
|
||||||
start.Attr = n.Attrs
|
|
||||||
start.Name = n.XMLName
|
|
||||||
|
|
||||||
err = e.EncodeToken(start)
|
|
||||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
const (
|
|
||||||
NSStream = "http://etherx.jabber.org/streams"
|
|
||||||
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
|
||||||
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
|
||||||
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
|
||||||
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
|
|
||||||
NSClient = "jabber:client"
|
|
||||||
NSComponent = "jabber:component:accept"
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
type Packet interface {
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attrs represents the common structure for base XMPP packets.
|
|
||||||
type Attrs struct {
|
|
||||||
Type StanzaType `xml:"type,attr,omitempty"`
|
|
||||||
Id string `xml:"id,attr,omitempty"`
|
|
||||||
From string `xml:"from,attr,omitempty"`
|
|
||||||
To string `xml:"to,attr,omitempty"`
|
|
||||||
Lang string `xml:"lang,attr,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type packetFormatter interface {
|
|
||||||
XMPPFormat() string
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
type StanzaType string
|
|
||||||
|
|
||||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
|
||||||
const (
|
|
||||||
IQTypeError StanzaType = "error"
|
|
||||||
IQTypeGet StanzaType = "get"
|
|
||||||
IQTypeResult StanzaType = "result"
|
|
||||||
IQTypeSet StanzaType = "set"
|
|
||||||
|
|
||||||
MessageTypeChat StanzaType = "chat"
|
|
||||||
MessageTypeError StanzaType = "error"
|
|
||||||
MessageTypeGroupchat StanzaType = "groupchat"
|
|
||||||
MessageTypeHeadline StanzaType = "headline"
|
|
||||||
MessageTypeNormal StanzaType = "normal" // Default
|
|
||||||
|
|
||||||
PresenceTypeError StanzaType = "error"
|
|
||||||
PresenceTypeProbe StanzaType = "probe"
|
|
||||||
PresenceTypeSubscribe StanzaType = "subscribe"
|
|
||||||
PresenceTypeSubscribed StanzaType = "subscribed"
|
|
||||||
PresenceTypeUnavailable StanzaType = "unavailable"
|
|
||||||
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
|
|
||||||
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
|
|
||||||
)
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reads and checks the opening XMPP stream element.
|
|
||||||
// TODO It returns a stream structure containing:
|
|
||||||
// - Host: You can check the host against the host you were expecting to connect to
|
|
||||||
// - Id: the Stream ID is a temporary shared secret used for some hash calculation. It is also used by ProcessOne
|
|
||||||
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
|
|
||||||
// getting through the authentication process.
|
|
||||||
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
|
|
||||||
func InitStream(p *xml.Decoder) (sessionID string, err error) {
|
|
||||||
for {
|
|
||||||
var t xml.Token
|
|
||||||
t, err = p.Token()
|
|
||||||
if err != nil {
|
|
||||||
return sessionID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch elem := t.(type) {
|
|
||||||
case xml.StartElement:
|
|
||||||
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
|
|
||||||
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
|
||||||
return sessionID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse XMPP stream attributes
|
|
||||||
for _, attrs := range elem.Attr {
|
|
||||||
switch attrs.Name.Local {
|
|
||||||
case "id":
|
|
||||||
sessionID = attrs.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sessionID, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextPacket scans XML token stream for next complete XMPP stanza.
|
|
||||||
// Once the type of stanza has been identified, a structure is created to decode
|
|
||||||
// that stanza and returned.
|
|
||||||
// TODO Use an interface to return packets interface xmppDecoder
|
|
||||||
// TODO make auth and bind use NextPacket instead of directly NextStart
|
|
||||||
func NextPacket(p *xml.Decoder) (Packet, error) {
|
|
||||||
// Read start element to find out how we want to parse the XMPP packet
|
|
||||||
se, err := NextStart(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode one of the top level XMPP namespace
|
|
||||||
switch se.Name.Space {
|
|
||||||
case NSStream:
|
|
||||||
return decodeStream(p, se)
|
|
||||||
case NSSASL:
|
|
||||||
return decodeSASL(p, se)
|
|
||||||
case NSClient:
|
|
||||||
return decodeClient(p, se)
|
|
||||||
case NSComponent:
|
|
||||||
return decodeComponent(p, se)
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unknown namespace " +
|
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan XML token stream to find next StartElement.
|
|
||||||
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
|
|
||||||
for {
|
|
||||||
t, err := p.Token()
|
|
||||||
if err == io.EOF {
|
|
||||||
return xml.StartElement{}, errors.New("connection closed")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
|
|
||||||
}
|
|
||||||
switch t := t.(type) {
|
|
||||||
case xml.StartElement:
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that
|
|
||||||
type.
|
|
||||||
That way, we have a consistent way to do type assertion, always matching against pointers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// decodeStream will fully decode a stream packet
|
|
||||||
func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
|
||||||
switch se.Name.Local {
|
|
||||||
case "error":
|
|
||||||
return streamError.decode(p, se)
|
|
||||||
case "features":
|
|
||||||
return streamFeatures.decode(p, se)
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unexpected XMPP packet " +
|
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeSASL decodes a packet related to SASL authentication.
|
|
||||||
func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
|
||||||
switch se.Name.Local {
|
|
||||||
case "success":
|
|
||||||
return saslSuccess.decode(p, se)
|
|
||||||
case "failure":
|
|
||||||
return saslFailure.decode(p, se)
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unexpected XMPP packet " +
|
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeClient decodes all known packets in the client namespace.
|
|
||||||
func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
|
||||||
switch se.Name.Local {
|
|
||||||
case "message":
|
|
||||||
return message.decode(p, se)
|
|
||||||
case "presence":
|
|
||||||
return presence.decode(p, se)
|
|
||||||
case "iq":
|
|
||||||
return iq.decode(p, se)
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unexpected XMPP packet " +
|
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeClient decodes all known packets in the component namespace.
|
|
||||||
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
|
||||||
switch se.Name.Local {
|
|
||||||
case "handshake": // handshake is used to authenticate components
|
|
||||||
return handshake.decode(p, se)
|
|
||||||
case "message":
|
|
||||||
return message.decode(p, se)
|
|
||||||
case "presence":
|
|
||||||
return presence.decode(p, se)
|
|
||||||
case "iq":
|
|
||||||
return iq.decode(p, se)
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unexpected XMPP packet " +
|
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Tune struct {
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/tune tune"`
|
|
||||||
Artist string `xml:"artist,omitempty"`
|
|
||||||
Length int `xml:"length,omitempty"`
|
|
||||||
Rating int `xml:"rating,omitempty"`
|
|
||||||
Source string `xml:"source,omitempty"`
|
|
||||||
Title string `xml:"title,omitempty"`
|
|
||||||
Track string `xml:"track,omitempty"`
|
|
||||||
Uri string `xml:"uri,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mood defines deta model for XEP-0107 - User Mood
|
|
||||||
// See: https://xmpp.org/extensions/xep-0107.html
|
|
||||||
type Mood struct {
|
|
||||||
MsgExtension // Mood can be added as a message extension
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/mood mood"`
|
|
||||||
// TODO: Custom parsing to extract mood type from tag name.
|
|
||||||
// Note: the list is predefined.
|
|
||||||
// Mood type
|
|
||||||
Text string `xml:"text,omitempty"`
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MUC Presence extension
|
|
||||||
|
|
||||||
// MucPresence implements XEP-0045: Multi-User Chat - 19.1
|
|
||||||
type MucPresence struct {
|
|
||||||
PresExtension
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/muc x"`
|
|
||||||
Password string `xml:"password,omitempty"`
|
|
||||||
History History `xml:"history,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeLayout = "2006-01-02T15:04:05Z"
|
|
||||||
|
|
||||||
// History implements XEP-0045: Multi-User Chat - 19.1
|
|
||||||
type History struct {
|
|
||||||
XMLName xml.Name
|
|
||||||
MaxChars NullableInt `xml:"maxchars,attr,omitempty"`
|
|
||||||
MaxStanzas NullableInt `xml:"maxstanzas,attr,omitempty"`
|
|
||||||
Seconds NullableInt `xml:"seconds,attr,omitempty"`
|
|
||||||
Since time.Time `xml:"since,attr,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NullableInt struct {
|
|
||||||
Value int
|
|
||||||
isSet bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNullableInt(val int) NullableInt {
|
|
||||||
return NullableInt{val, true}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NullableInt) Get() (v int, ok bool) {
|
|
||||||
return n.Value, n.isSet
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing for history element
|
|
||||||
func (h *History) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
h.XMLName = start.Name
|
|
||||||
|
|
||||||
// Extract attributes
|
|
||||||
for _, attr := range start.Attr {
|
|
||||||
switch attr.Name.Local {
|
|
||||||
case "maxchars":
|
|
||||||
v, err := strconv.Atoi(attr.Value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.MaxChars = NewNullableInt(v)
|
|
||||||
case "maxstanzas":
|
|
||||||
v, err := strconv.Atoi(attr.Value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.MaxStanzas = NewNullableInt(v)
|
|
||||||
case "seconds":
|
|
||||||
v, err := strconv.Atoi(attr.Value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.Seconds = NewNullableInt(v)
|
|
||||||
case "since":
|
|
||||||
t, err := time.Parse(timeLayout, attr.Value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.Since = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume remaining data until element end
|
|
||||||
for {
|
|
||||||
t, err := d.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tt := t.(type) {
|
|
||||||
case xml.EndElement:
|
|
||||||
if tt == start.End() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
|
||||||
mc, isMcSet := h.MaxChars.Get()
|
|
||||||
ms, isMsSet := h.MaxStanzas.Get()
|
|
||||||
s, isSSet := h.Seconds.Get()
|
|
||||||
|
|
||||||
// We do not have any value, ignore history element
|
|
||||||
if h.Since.IsZero() && !isMcSet && !isMsSet && !isSSet {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode start element and attributes
|
|
||||||
start.Name = xml.Name{Local: "history"}
|
|
||||||
|
|
||||||
if isMcSet {
|
|
||||||
attr := xml.Attr{
|
|
||||||
Name: xml.Name{Local: "maxchars"},
|
|
||||||
Value: strconv.Itoa(mc),
|
|
||||||
}
|
|
||||||
start.Attr = append(start.Attr, attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isMsSet {
|
|
||||||
attr := xml.Attr{
|
|
||||||
Name: xml.Name{Local: "maxstanzas"},
|
|
||||||
Value: strconv.Itoa(ms),
|
|
||||||
}
|
|
||||||
start.Attr = append(start.Attr, attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSSet {
|
|
||||||
attr := xml.Attr{
|
|
||||||
Name: xml.Name{Local: "seconds"},
|
|
||||||
Value: strconv.Itoa(s),
|
|
||||||
}
|
|
||||||
start.Attr = append(start.Attr, attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.Since.IsZero() {
|
|
||||||
attr := xml.Attr{
|
|
||||||
Name: xml.Name{Local: "since"},
|
|
||||||
Value: h.Since.Format(timeLayout),
|
|
||||||
}
|
|
||||||
start.Attr = append(start.Attr, attr)
|
|
||||||
}
|
|
||||||
if err := e.EncodeToken(start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTPresence, xml.Name{"http://jabber.org/protocol/muc", "x"}, MucPresence{})
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package stanza_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://xmpp.org/extensions/xep-0045.html#example-27
|
|
||||||
func TestMucPassword(t *testing.T) {
|
|
||||||
str := `<presence
|
|
||||||
from='hag66@shakespeare.lit/pda'
|
|
||||||
id='djn4714'
|
|
||||||
to='coven@chat.shakespeare.lit/thirdwitch'>
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc'>
|
|
||||||
<password>cauldronburn</password>
|
|
||||||
</x>
|
|
||||||
</presence>`
|
|
||||||
|
|
||||||
var parsedPresence stanza.Presence
|
|
||||||
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error", str)
|
|
||||||
}
|
|
||||||
|
|
||||||
var muc stanza.MucPresence
|
|
||||||
if ok := parsedPresence.Get(&muc); !ok {
|
|
||||||
t.Error("muc presence extension was not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if muc.Password != "cauldronburn" {
|
|
||||||
t.Errorf("incorrect password: '%s'", muc.Password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://xmpp.org/extensions/xep-0045.html#example-37
|
|
||||||
func TestMucHistory(t *testing.T) {
|
|
||||||
str := `<presence
|
|
||||||
from='hag66@shakespeare.lit/pda'
|
|
||||||
id='n13mt3l'
|
|
||||||
to='coven@chat.shakespeare.lit/thirdwitch'>
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc'>
|
|
||||||
<history maxstanzas='20'/>
|
|
||||||
</x>
|
|
||||||
</presence>`
|
|
||||||
|
|
||||||
var parsedPresence stanza.Presence
|
|
||||||
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error: %s", str, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var muc stanza.MucPresence
|
|
||||||
if ok := parsedPresence.Get(&muc); !ok {
|
|
||||||
t.Error("muc presence extension was not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := muc.History.MaxStanzas.Get(); !ok || v != 20 {
|
|
||||||
t.Errorf("incorrect MaxStanzas: '%#v'", muc.History.MaxStanzas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://xmpp.org/extensions/xep-0045.html#example-37
|
|
||||||
func TestMucNoHistory(t *testing.T) {
|
|
||||||
str := "<presence" +
|
|
||||||
" id=\"n13mt3l\"" +
|
|
||||||
" from=\"hag66@shakespeare.lit/pda\"" +
|
|
||||||
" to=\"coven@chat.shakespeare.lit/thirdwitch\">" +
|
|
||||||
"<x xmlns=\"http://jabber.org/protocol/muc\">" +
|
|
||||||
"<history maxstanzas=\"0\"></history>" +
|
|
||||||
"</x>" +
|
|
||||||
"</presence>"
|
|
||||||
|
|
||||||
maxstanzas := 0
|
|
||||||
|
|
||||||
pres := stanza.Presence{Attrs: stanza.Attrs{
|
|
||||||
From: "hag66@shakespeare.lit/pda",
|
|
||||||
Id: "n13mt3l",
|
|
||||||
To: "coven@chat.shakespeare.lit/thirdwitch",
|
|
||||||
},
|
|
||||||
Extensions: []stanza.PresExtension{
|
|
||||||
stanza.MucPresence{
|
|
||||||
History: stanza.History{MaxStanzas: stanza.NewNullableInt(maxstanzas)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := xml.Marshal(&pres)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("error on encode:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(data) != str {
|
|
||||||
t.Errorf("incorrect stanza: \n%s\n%s", str, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Presence Packet
|
|
||||||
|
|
||||||
// Presence implements RFC 6120 - A.5 Client Namespace (a part)
|
|
||||||
type Presence struct {
|
|
||||||
XMLName xml.Name `xml:"presence"`
|
|
||||||
Attrs
|
|
||||||
Show PresenceShow `xml:"show,omitempty"`
|
|
||||||
Status string `xml:"status,omitempty"`
|
|
||||||
Priority int8 `xml:"priority,omitempty"` // default: 0
|
|
||||||
Error Err `xml:"error,omitempty"`
|
|
||||||
Extensions []PresExtension `xml:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Presence) Name() string {
|
|
||||||
return "presence"
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPresence(a Attrs) Presence {
|
|
||||||
return Presence{
|
|
||||||
XMLName: xml.Name{Local: "presence"},
|
|
||||||
Attrs: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get search and extracts a specific extension on a presence stanza.
|
|
||||||
// It receives a pointer to an PresExtension. 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 presence.
|
|
||||||
//
|
|
||||||
// Example usage:
|
|
||||||
// var muc xmpp.MucPresence
|
|
||||||
// if ok := msg.Get(&muc); ok {
|
|
||||||
// // muc presence extension has been found
|
|
||||||
// }
|
|
||||||
func (pres *Presence) Get(ext PresExtension) bool {
|
|
||||||
target := reflect.ValueOf(ext)
|
|
||||||
if target.Kind() != reflect.Ptr {
|
|
||||||
panic("you must pass a pointer to the message Get method")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range pres.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 presenceDecoder struct{}
|
|
||||||
|
|
||||||
var presence presenceDecoder
|
|
||||||
|
|
||||||
func (presenceDecoder) decode(p *xml.Decoder, se xml.StartElement) (Presence, error) {
|
|
||||||
var packet Presence
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
// TODO Add default presence type (when omitted)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing for presence stanza
|
|
||||||
func (pres *Presence) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
pres.XMLName = start.Name
|
|
||||||
|
|
||||||
// Extract packet attributes
|
|
||||||
for _, attr := range start.Attr {
|
|
||||||
if attr.Name.Local == "id" {
|
|
||||||
pres.Id = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "type" {
|
|
||||||
pres.Type = StanzaType(attr.Value)
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "to" {
|
|
||||||
pres.To = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "from" {
|
|
||||||
pres.From = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "lang" {
|
|
||||||
pres.Lang = attr.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode inner elements
|
|
||||||
for {
|
|
||||||
t, err := d.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tt := t.(type) {
|
|
||||||
|
|
||||||
case xml.StartElement:
|
|
||||||
if presExt := TypeRegistry.GetPresExtension(tt.Name); presExt != nil {
|
|
||||||
// Decode message extension
|
|
||||||
err = d.DecodeElement(presExt, &tt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pres.Extensions = append(pres.Extensions, presExt)
|
|
||||||
} else {
|
|
||||||
// Decode standard message sub-elements
|
|
||||||
var err error
|
|
||||||
switch tt.Name.Local {
|
|
||||||
case "show":
|
|
||||||
err = d.DecodeElement(&pres.Show, &tt)
|
|
||||||
case "status":
|
|
||||||
err = d.DecodeElement(&pres.Status, &tt)
|
|
||||||
case "priority":
|
|
||||||
err = d.DecodeElement(&pres.Priority, &tt)
|
|
||||||
case "error":
|
|
||||||
err = d.DecodeElement(&pres.Error, &tt)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case xml.EndElement:
|
|
||||||
if tt == start.End() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
// PresenceShow is a Enum of presence element show
|
|
||||||
type PresenceShow string
|
|
||||||
|
|
||||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
|
||||||
const (
|
|
||||||
PresenceShowAway PresenceShow = "away"
|
|
||||||
PresenceShowChat PresenceShow = "chat"
|
|
||||||
PresenceShowDND PresenceShow = "dnd"
|
|
||||||
PresenceShowXA PresenceShow = "xa"
|
|
||||||
)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package stanza_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGeneratePresence(t *testing.T) {
|
|
||||||
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
|
||||||
presence.Show = stanza.PresenceShowChat
|
|
||||||
|
|
||||||
data, err := xml.Marshal(presence)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot marshal xml structure")
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedPresence stanza.Presence
|
|
||||||
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !xmlEqual(parsedPresence, presence) {
|
|
||||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedPresence, presence))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPresenceSubElt(t *testing.T) {
|
|
||||||
// Test structure to ensure that show, status and priority are correctly defined as presence
|
|
||||||
// package sub-elements
|
|
||||||
type pres struct {
|
|
||||||
Show stanza.PresenceShow `xml:"show"`
|
|
||||||
Status string `xml:"status"`
|
|
||||||
Priority int8 `xml:"priority"`
|
|
||||||
}
|
|
||||||
|
|
||||||
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
|
||||||
presence.Show = stanza.PresenceShowXA
|
|
||||||
presence.Status = "Coding"
|
|
||||||
presence.Priority = 10
|
|
||||||
|
|
||||||
data, err := xml.Marshal(presence)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot marshal xml structure")
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedPresence pres
|
|
||||||
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedPresence.Show != presence.Show {
|
|
||||||
t.Errorf("cannot read 'show' as presence subelement (%s)", parsedPresence.Show)
|
|
||||||
}
|
|
||||||
if parsedPresence.Status != presence.Status {
|
|
||||||
t.Errorf("cannot read 'status' as presence subelement (%s)", parsedPresence.Status)
|
|
||||||
}
|
|
||||||
if parsedPresence.Priority != presence.Priority {
|
|
||||||
t.Errorf("cannot read 'priority' as presence subelement (%d)", parsedPresence.Priority)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PubSub struct {
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
|
|
||||||
Publish *Publish
|
|
||||||
Retract *Retract
|
|
||||||
// TODO <configure/>
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PubSub) Namespace() string {
|
|
||||||
return p.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
type Publish struct {
|
|
||||||
XMLName xml.Name `xml:"publish"`
|
|
||||||
Node string `xml:"node,attr"`
|
|
||||||
Item Item
|
|
||||||
}
|
|
||||||
|
|
||||||
type Item struct {
|
|
||||||
XMLName xml.Name `xml:"item"`
|
|
||||||
Id string `xml:"id,attr,omitempty"`
|
|
||||||
Tune *Tune
|
|
||||||
Mood *Mood
|
|
||||||
}
|
|
||||||
|
|
||||||
type Retract struct {
|
|
||||||
XMLName xml.Name `xml:"retract"`
|
|
||||||
Node string `xml:"node,attr"`
|
|
||||||
Notify string `xml:"notify,attr"`
|
|
||||||
Item Item
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{})
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MsgExtension interface{}
|
|
||||||
type PresExtension interface{}
|
|
||||||
|
|
||||||
// The Registry for msg and IQ types is a global variable.
|
|
||||||
// TODO: Move to the client init process to remove the dependency on a global variable.
|
|
||||||
// That should make it possible to be able to share the decoder.
|
|
||||||
// TODO: Ensure that a client can add its own custom namespace to the registry (or overload existing ones).
|
|
||||||
|
|
||||||
type PacketType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
PKTPresence PacketType = iota
|
|
||||||
PKTMessage
|
|
||||||
PKTIQ
|
|
||||||
)
|
|
||||||
|
|
||||||
var TypeRegistry = newRegistry()
|
|
||||||
|
|
||||||
// We store different registries per packet type and namespace.
|
|
||||||
type registryKey struct {
|
|
||||||
packetType PacketType
|
|
||||||
namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
type registryForNamespace map[string]reflect.Type
|
|
||||||
|
|
||||||
type registry struct {
|
|
||||||
// We store different registries per packet type and namespace.
|
|
||||||
msgTypes map[registryKey]registryForNamespace
|
|
||||||
// Handle concurrent access
|
|
||||||
msgTypesLock *sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRegistry() *registry {
|
|
||||||
return ®istry{
|
|
||||||
msgTypes: make(map[registryKey]registryForNamespace),
|
|
||||||
msgTypesLock: &sync.RWMutex{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MapExtension stores extension type for packet payload.
|
|
||||||
// The match is done per PacketType (iq, message, or presence) and XML tag name.
|
|
||||||
// You can use the alias "*" as local XML name to be able to match all unknown tag name for that
|
|
||||||
// packet type and namespace.
|
|
||||||
func (r *registry) MapExtension(pktType PacketType, name xml.Name, extension MsgExtension) {
|
|
||||||
key := registryKey{pktType, name.Space}
|
|
||||||
r.msgTypesLock.RLock()
|
|
||||||
store := r.msgTypes[key]
|
|
||||||
r.msgTypesLock.RUnlock()
|
|
||||||
|
|
||||||
r.msgTypesLock.Lock()
|
|
||||||
defer r.msgTypesLock.Unlock()
|
|
||||||
if store == nil {
|
|
||||||
store = make(map[string]reflect.Type)
|
|
||||||
}
|
|
||||||
store[name.Local] = reflect.TypeOf(extension)
|
|
||||||
r.msgTypes[key] = store
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExtensionType returns extension type for packet payload, based on packet type and tag name.
|
|
||||||
func (r *registry) GetExtensionType(pktType PacketType, name xml.Name) reflect.Type {
|
|
||||||
key := registryKey{pktType, name.Space}
|
|
||||||
|
|
||||||
r.msgTypesLock.RLock()
|
|
||||||
defer r.msgTypesLock.RUnlock()
|
|
||||||
store := r.msgTypes[key]
|
|
||||||
result := store[name.Local]
|
|
||||||
if result == nil && name.Local != "*" {
|
|
||||||
return store["*"]
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPresExtension returns an instance of PresExtension, by matching packet type and XML
|
|
||||||
// tag name against the registry.
|
|
||||||
func (r *registry) GetPresExtension(name xml.Name) PresExtension {
|
|
||||||
if extensionType := r.GetExtensionType(PKTPresence, name); extensionType != nil {
|
|
||||||
val := reflect.New(extensionType)
|
|
||||||
elt := val.Interface()
|
|
||||||
if presExt, ok := elt.(PresExtension); ok {
|
|
||||||
return presExt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMsgExtension returns an instance of MsgExtension, by matching packet type and XML
|
|
||||||
// tag name against the registry.
|
|
||||||
func (r *registry) GetMsgExtension(name xml.Name) MsgExtension {
|
|
||||||
if extensionType := r.GetExtensionType(PKTMessage, name); extensionType != nil {
|
|
||||||
val := reflect.New(extensionType)
|
|
||||||
elt := val.Interface()
|
|
||||||
if msgExt, ok := elt.(MsgExtension); ok {
|
|
||||||
return msgExt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIQExtension returns an instance of IQPayload, by matching packet type and XML
|
|
||||||
// tag name against the registry.
|
|
||||||
func (r *registry) GetIQExtension(name xml.Name) IQPayload {
|
|
||||||
if extensionType := r.GetExtensionType(PKTIQ, name); extensionType != nil {
|
|
||||||
val := reflect.New(extensionType)
|
|
||||||
elt := val.Interface()
|
|
||||||
if iqExt, ok := elt.(IQPayload); ok {
|
|
||||||
return iqExt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRegistry_RegisterMsgExt(t *testing.T) {
|
|
||||||
// Setup registry
|
|
||||||
typeRegistry := newRegistry()
|
|
||||||
|
|
||||||
// Register an element
|
|
||||||
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
|
|
||||||
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
|
|
||||||
|
|
||||||
// Match that element
|
|
||||||
receipt := typeRegistry.GetMsgExtension(name)
|
|
||||||
if receipt == nil {
|
|
||||||
t.Error("cannot read element type from registry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r := receipt.(type) {
|
|
||||||
case *ReceiptRequest:
|
|
||||||
default:
|
|
||||||
t.Errorf("Registry did not return expected type ReceiptRequest: %v", reflect.TypeOf(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkRegistryGet(b *testing.B) {
|
|
||||||
// Setup registry
|
|
||||||
typeRegistry := newRegistry()
|
|
||||||
|
|
||||||
// Register an element
|
|
||||||
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
|
|
||||||
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
// Match that element
|
|
||||||
receipt := typeRegistry.GetExtensionType(PKTMessage, name)
|
|
||||||
if receipt == nil {
|
|
||||||
b.Error("cannot read element type from registry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import "encoding/xml"
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// SASLAuth implements SASL Authentication initiation.
|
|
||||||
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.2
|
|
||||||
type SASLAuth struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
|
||||||
Mechanism string `xml:"mechanism,attr"`
|
|
||||||
Value string `xml:",innerxml"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// SASLSuccess implements SASL Success nonza, sent by server as a result of the
|
|
||||||
// SASL auth negotiation.
|
|
||||||
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.6
|
|
||||||
type SASLSuccess struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (SASLSuccess) Name() string {
|
|
||||||
return "sasl:success"
|
|
||||||
}
|
|
||||||
|
|
||||||
// SASLSuccess decoding
|
|
||||||
type saslSuccessDecoder struct{}
|
|
||||||
|
|
||||||
var saslSuccess saslSuccessDecoder
|
|
||||||
|
|
||||||
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
|
|
||||||
var packet SASLSuccess
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// SASLFailure
|
|
||||||
type SASLFailure struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
|
||||||
Any xml.Name // error reason is a subelement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (SASLFailure) Name() string {
|
|
||||||
return "sasl:failure"
|
|
||||||
}
|
|
||||||
|
|
||||||
// SASLFailure decoding
|
|
||||||
type saslFailureDecoder struct{}
|
|
||||||
|
|
||||||
var saslFailure saslFailureDecoder
|
|
||||||
|
|
||||||
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
|
|
||||||
var packet SASLFailure
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// Resource binding
|
|
||||||
|
|
||||||
// Bind is an IQ payload used during session negotiation to bind user resource
|
|
||||||
// to the current XMPP stream.
|
|
||||||
// Reference: https://tools.ietf.org/html/rfc6120#section-7
|
|
||||||
type Bind struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
|
||||||
Resource string `xml:"resource,omitempty"`
|
|
||||||
Jid string `xml:"jid,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bind) Namespace() string {
|
|
||||||
return b.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Session (Obsolete)
|
|
||||||
|
|
||||||
// Session is both a stream feature and an obsolete IQ Payload, used to bind a
|
|
||||||
// resource to the current XMPP stream on RFC 3121 only XMPP servers.
|
|
||||||
// Session is obsolete in RFC 6121. It is added to Fluux XMPP for compliance
|
|
||||||
// with RFC 3121.
|
|
||||||
// Reference: https://xmpp.org/rfcs/rfc3921.html#session
|
|
||||||
//
|
|
||||||
// This is the draft defining how to handle the transition:
|
|
||||||
// https://tools.ietf.org/html/draft-cridland-xmpp-session-01
|
|
||||||
type StreamSession struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
|
||||||
Optional bool // If element does exist, it mean we are not required to open session
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StreamSession) Namespace() string {
|
|
||||||
return s.XMLName.Space
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StreamSession) IsOptional() bool {
|
|
||||||
if s.XMLName.Local == "session" {
|
|
||||||
return s.Optional
|
|
||||||
}
|
|
||||||
// If session element is missing, then we should not use session
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Registry init
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, Bind{})
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-session", "session"}, StreamSession{})
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package stanza_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check that we can detect optional session from advertised stream features
|
|
||||||
func TestSessionFeatures(t *testing.T) {
|
|
||||||
streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: true}}
|
|
||||||
|
|
||||||
data, err := xml.Marshal(streamFeatures)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot marshal xml structure: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedStream := stanza.StreamFeatures{}
|
|
||||||
if err = xml.Unmarshal(data, &parsedStream); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !parsedStream.Session.IsOptional() {
|
|
||||||
t.Error("Session should be optional")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the Session tag can be used in IQ decoding
|
|
||||||
func TestSessionIQ(t *testing.T) {
|
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"})
|
|
||||||
iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: true}
|
|
||||||
|
|
||||||
data, err := xml.Marshal(iq)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot marshal xml structure: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedIQ := stanza.IQ{}
|
|
||||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session, ok := parsedIQ.Payload.(*stanza.StreamSession)
|
|
||||||
if !ok {
|
|
||||||
t.Error("Missing session payload")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !session.IsOptional() {
|
|
||||||
t.Error("Session should be optional")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Test Sasl mechanism
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Used during stream initiation / session establishment
|
|
||||||
type TLSProceed struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type tlsFailure struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
package stanza
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// StreamFeatures Packet
|
|
||||||
// Reference: The active stream features are published on
|
|
||||||
// https://xmpp.org/registrar/stream-features.html
|
|
||||||
// Note: That page misses draft and experimental XEP (i.e CSI, etc)
|
|
||||||
|
|
||||||
type StreamFeatures struct {
|
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
|
||||||
// Server capabilities hash
|
|
||||||
Caps Caps
|
|
||||||
// Stream features
|
|
||||||
StartTLS tlsStartTLS
|
|
||||||
Mechanisms saslMechanisms
|
|
||||||
Bind Bind
|
|
||||||
StreamManagement streamManagement
|
|
||||||
// Obsolete
|
|
||||||
Session StreamSession
|
|
||||||
// ProcessOne Stream Features
|
|
||||||
P1Push p1Push
|
|
||||||
P1Rebind p1Rebind
|
|
||||||
p1Ack p1Ack
|
|
||||||
Any []xml.Name `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (StreamFeatures) Name() string {
|
|
||||||
return "stream:features"
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamFeatureDecoder struct{}
|
|
||||||
|
|
||||||
var streamFeatures streamFeatureDecoder
|
|
||||||
|
|
||||||
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
|
|
||||||
var packet StreamFeatures
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capabilities
|
|
||||||
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
|
|
||||||
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
|
|
||||||
// and peer servers do not need to send service discovery requests each time they connect."
|
|
||||||
// This is not a stream feature but a way to let client cache server disco info.
|
|
||||||
type Caps struct {
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
|
|
||||||
Hash string `xml:"hash,attr"`
|
|
||||||
Node string `xml:"node,attr"`
|
|
||||||
Ver string `xml:"ver,attr"`
|
|
||||||
Ext string `xml:"ext,attr,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Supported Stream Features
|
|
||||||
|
|
||||||
// StartTLS feature
|
|
||||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
|
|
||||||
type tlsStartTLS struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing startTLS required flag
|
|
||||||
func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
stls.XMLName = start.Name
|
|
||||||
|
|
||||||
// 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:
|
|
||||||
elt := new(Node)
|
|
||||||
|
|
||||||
err = d.DecodeElement(elt, &tt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if elt.XMLName.Local == "required" {
|
|
||||||
stls.Required = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case xml.EndElement:
|
|
||||||
if tt == start.End() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStartTLS() (feature tlsStartTLS, isSupported bool) {
|
|
||||||
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
|
||||||
return sf.StartTLS, true
|
|
||||||
}
|
|
||||||
return feature, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mechanisms
|
|
||||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
|
|
||||||
type saslMechanisms struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
|
||||||
Mechanism []string `xml:"mechanism"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamManagement
|
|
||||||
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
|
|
||||||
type streamManagement struct {
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
|
||||||
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// P1 extensions
|
|
||||||
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
|
|
||||||
|
|
||||||
// p1:push support
|
|
||||||
type p1Push struct {
|
|
||||||
XMLName xml.Name `xml:"p1:push push"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1:rebind suppor
|
|
||||||
type p1Rebind struct {
|
|
||||||
XMLName xml.Name `xml:"p1:rebind rebind"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1:ack support
|
|
||||||
type p1Ack struct {
|
|
||||||
XMLName xml.Name `xml:"p1:ack ack"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// StreamError Packet
|
|
||||||
|
|
||||||
type StreamError struct {
|
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
|
|
||||||
Error xml.Name `xml:",any"`
|
|
||||||
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (StreamError) Name() string {
|
|
||||||
return "stream:error"
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamErrorDecoder struct{}
|
|
||||||
|
|
||||||
var streamError streamErrorDecoder
|
|
||||||
|
|
||||||
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
|
|
||||||
var packet StreamError
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package stanza_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNoStartTLS(t *testing.T) {
|
|
||||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
|
||||||
</stream:features>`
|
|
||||||
|
|
||||||
var parsedSF stanza.StreamFeatures
|
|
||||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
startTLS, ok := parsedSF.DoesStartTLS()
|
|
||||||
if ok {
|
|
||||||
t.Error("StartTLS feature should not be enabled")
|
|
||||||
}
|
|
||||||
if startTLS.Required {
|
|
||||||
t.Error("StartTLS cannot be required as default")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStartTLS(t *testing.T) {
|
|
||||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
|
||||||
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
|
|
||||||
<required/>
|
|
||||||
</starttls>
|
|
||||||
</stream:features>`
|
|
||||||
|
|
||||||
var parsedSF stanza.StreamFeatures
|
|
||||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
startTLS, ok := parsedSF.DoesStartTLS()
|
|
||||||
if !ok {
|
|
||||||
t.Error("StartTLS feature should be enabled")
|
|
||||||
}
|
|
||||||
if !startTLS.Required {
|
|
||||||
t.Error("StartTLS feature should be required")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Ability to support / detect previous version of stream management feature
|
|
||||||
func TestStreamManagement(t *testing.T) {
|
|
||||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
|
||||||
<sm xmlns='urn:xmpp:sm:3'/>
|
|
||||||
</stream:features>`
|
|
||||||
|
|
||||||
var parsedSF stanza.StreamFeatures
|
|
||||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ok := parsedSF.DoesStreamManagement()
|
|
||||||
if !ok {
|
|
||||||
t.Error("Stream Management feature should have been detected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user