forked from jshiffer/go-xmpp
Compare commits
294 Commits
370c500a5e
..
godoc
| Author | SHA1 | Date | |
|---|---|---|---|
| 79803a8af9 | |||
| 604d2c6c1e | |||
| 7c71d93026 | |||
| cca0919b8a | |||
| 40e907e8ee | |||
| 838c059398 | |||
| 3ba59afd6e | |||
| 661188752e | |||
| 409d563eec | |||
| d90cc239ae | |||
| b35868b689 | |||
| 6165232d7a | |||
| 91c562200d | |||
| 5992cc2231 | |||
| 318e5e8a50 | |||
| a465e370e2 | |||
| 9bb4f32769 | |||
| e3c0747cbb | |||
| 0fd1bb2483 | |||
| 4a4fc39cf6 | |||
| 5db9a80605 | |||
| 20a66dc47d | |||
| 1dacc663d3 | |||
| cb9016693c | |||
| 0c7e4588c6 | |||
| 3fa1a4b387 | |||
| 80f32b4af7 | |||
| 781b875cf1 | |||
| 3d088a6078 | |||
| 0ee4764d31 | |||
| 1971772394 | |||
| 6fbfe9fd0a | |||
| 5ed66de79e | |||
| 428787d7ab | |||
| 0acf824217 | |||
| 445bb8efa3 | |||
| f79a3a219b | |||
| 1c792e61c6 | |||
| fde524ef98 | |||
| 7a386ec8d0 | |||
| 83f96fbd41 | |||
| def9629a0b | |||
| 1542110f1b | |||
| d6d371df4d | |||
| 3521c488ea | |||
| 8f7b4ba8a4 | |||
| 4a4c4850d1 | |||
| 6ddfa781e5 | |||
| 555cbe12b4 | |||
| e9c704eff5 | |||
| d9fdff0839 | |||
| 145fce6b3f | |||
| 5d362b505b | |||
| 923fd61587 | |||
| 44681e8053 | |||
| 1a7aa94bae | |||
| a6cbc0c08f | |||
| 3f81465c6c | |||
| 24502f7cd7 | |||
| af0ae525b8 | |||
| d455f29258 | |||
| 683fdea2ec | |||
| 7f889909fd | |||
| 4d015e5b29 | |||
| c8ded1462f | |||
| 28ae759144 | |||
| 55c7251fac | |||
| 398ba224e7 | |||
| 00e9dd4e47 | |||
| ddff6527bd | |||
| 9219bf5aa9 | |||
| 715bf6976f | |||
| 348f29e055 | |||
| 45c7ca74b1 | |||
| 7aef8357ed | |||
| 2c7b03fcea | |||
| 9b57809e9d | |||
| f0f0d5a285 | |||
| 61cdac89e0 | |||
| c6f0d03f60 | |||
| cc2fa7307f | |||
| 9db33d5792 | |||
| b05e68c844 | |||
| f7b7482d2e | |||
| 355401aa84 | |||
| eb54ec9fb1 | |||
| 4d4710463d | |||
| 2af9521036 | |||
| 30e6adc073 | |||
| 709a95129e | |||
| 44568fcf2b | |||
| 08bb9965b8 | |||
| 322a6594e7 | |||
| 45cb2e6f34 | |||
| 411619c2ef | |||
| 36e3379f5a | |||
| bc2fad6693 | |||
| 909cf753c9 | |||
| 83ae778d33 | |||
| 6fc12e9779 | |||
| 2d95ca9384 | |||
| 736a60cd1b | |||
| 021f6d3740 | |||
| 54dfa60f12 | |||
| 36900cee20 | |||
| d4a8616da2 | |||
| b7461ae97f | |||
| 3689448c90 | |||
| 0865f4e35c | |||
| eb2b506e3b | |||
| ae153e1ee5 | |||
| 1be04b0fba | |||
| 269f78b30d | |||
| 2d8d4516fd | |||
| d45dd6a44a | |||
| b8fdc510a6 | |||
| 3ccc2680b0 | |||
| 3ea0e38f98 | |||
| b7c21871b1 | |||
| a451e64638 | |||
| 2f391fde80 | |||
| 6cdadc95e9 | |||
| b93a3a2550 | |||
| 80d8d6d231 | |||
| f034b74b54 | |||
| e7c57cad97 | |||
| 4e597505f4 | |||
| 15ceab9fc4 | |||
| 4c23014051 | |||
| 06ee607f53 | |||
| 0e110bc412 | |||
| 57ed387f4f | |||
| 67eaed98b6 | |||
| 7a4364be95 | |||
| 836e723273 | |||
| b05efea81d | |||
| f74f276a22 | |||
| 0f6ff41792 | |||
| b3a6429e0e | |||
| e54260ec68 | |||
| 996feb1a40 | |||
| b62533d005 | |||
| d31fc9b34c | |||
| afe2017b8b | |||
| c55257cbed | |||
| f390433700 | |||
| 757e339946 | |||
| 95dded61a1 | |||
| da0a8b9c29 | |||
| 53916900d4 | |||
| 5d6329f0b4 | |||
| 23a710b36f | |||
| 1a6e4f266b | |||
| f45829916c | |||
| c642ad79fc | |||
| d16c4cbba4 | |||
| 67d9170354 | |||
| 91a7cc9c64 | |||
| ffcde39ba6 | |||
| 392d3a1ae7 | |||
| da4ae4693e | |||
| 48bc14b3e0 | |||
| adf2c13a8c | |||
| e1cb9ac037 | |||
| c0f3d20440 | |||
| cf836f5f71 | |||
| b030e8dd4b | |||
| 140d3b4d95 | |||
| fab47e1a4b | |||
| 94d9cbf7fa | |||
| 401f0be40c | |||
| 9bebbda379 | |||
| a35a7959d7 | |||
| be587fac4c | |||
| f837b8be87 | |||
| c7cdf3b5f3 | |||
| 590eed1d07 | |||
| 5eae7f4ef7 | |||
| 3a51dce786 | |||
| 95585866c2 | |||
| f70e2ca9a7 | |||
| fa5590e921 | |||
| 1c3aaad174 | |||
| a43518b976 | |||
| 07b0d2d14d | |||
| d6bedfb033 | |||
| 24b8d7da3d | |||
| bb1621364a | |||
| c451e3bc63 | |||
| 266ed9b1e4 | |||
| ad6e09a0f6 | |||
| 2cd8eed765 | |||
| 3e6cf2c8b0 | |||
| bdfd035bf3 | |||
| 4173d9ee70 | |||
| b9b77f6be9 | |||
| ca148e5fe5 | |||
| 7e50d313ea | |||
| 28fb5bf61b | |||
| 7ae2adca9f | |||
| 8cb1e1264e | |||
| cb2af43fe3 | |||
| 57cc0a25ac | |||
| adb14260f0 | |||
| fb8d050a00 | |||
| 8470c01c09 | |||
| bbfafbb32c | |||
| 993ca630f7 | |||
| 80f2888cff | |||
| d33490cdc0 | |||
| 2e47f1659d | |||
| 20c2c44941 | |||
| b3c11fb151 | |||
| c821267928 | |||
| dade3504f0 | |||
| ff2da776d3 | |||
| ceeb51ce0e | |||
| 94815de173 | |||
| e14f58d9a9 | |||
| ec95020ac2 | |||
| 10219ec1e6 | |||
| 01063ec284 | |||
| d2765aec15 | |||
| 24ac2c0526 | |||
| fde0faca09 | |||
| b21fee420f | |||
| 90865aeb8e | |||
| b31c29a03d | |||
| ec68a04554 | |||
| ce61a253af | |||
| 8a611050b4 | |||
| 75c9416763 | |||
| 5c291c13b5 | |||
| 23d91551c0 | |||
| 302e971773 | |||
| 51d6759354 | |||
| fb5911564c | |||
| 06cb1804a8 | |||
| 76a6d35a8b | |||
| 4016e15a6a | |||
| d13e87f5bb | |||
| fef7d1ec50 | |||
| 843059b096 | |||
| a6b003ccd3 | |||
| 753a872fe8 | |||
| 1ea560ba1e | |||
| f1cda2c899 | |||
| 9f0a26f9d8 | |||
| 4f4a106602 | |||
| 710174b165 | |||
| 083b9c7755 | |||
| 1154df3f97 | |||
| d5221f1a11 | |||
| 683085125c | |||
| e51fffcaed | |||
| 4ac645a9ec | |||
| 228cb14491 | |||
| 2579c84481 | |||
| b186813e91 | |||
| 62fc9a407e | |||
| cb388cd89e | |||
| d4f193a1bd | |||
| c41ed1c32f | |||
| d26d066540 | |||
| a2aab652a9 | |||
| 82c01de54b | |||
| 06beebc812 | |||
| 09bf85b1e8 | |||
| e6a645dee0 | |||
| 6fdef748be | |||
| 57835bfcb5 | |||
| 268acbff07 | |||
| 3a516a43d3 | |||
| adcd2bd467 | |||
| dc30c70c17 | |||
| 4d3463458e | |||
| e29241b09d | |||
| 14666841aa | |||
| 5949967daf | |||
| f7651c7785 | |||
| 190b1b53a6 | |||
| ac881fa6a4 | |||
| 3ca015f307 | |||
| 30312aa82a | |||
| 05297ce475 | |||
| c392810d29 | |||
| 1b90d5d2ef | |||
| ff334ba729 | |||
| 2da7c27d18 | |||
| c5732bbf1a | |||
| f237b861bb | |||
| d8c8419cb1 | |||
| 6f15887129 | |||
| c9340b668d |
Executable
+36
@@ -0,0 +1,36 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
coverage.out
|
||||
coverage.txt
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
.DS_Store
|
||||
|
||||
# Do not commit codeship key
|
||||
codeship.aes
|
||||
codeship.env
|
||||
|
||||
priv/
|
||||
@@ -1,5 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- tip
|
||||
script:
|
||||
- go test
|
||||
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at contact@process-one.net. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# Contributing
|
||||
|
||||
We'd love for you to contribute to our source code and to make our project even better than it is
|
||||
today! Here are the guidelines we'd like you to follow:
|
||||
|
||||
* [Code of Conduct](#coc)
|
||||
* [Questions and Problems](#question)
|
||||
* [Issues and Bugs](#issue)
|
||||
* [Feature Requests](#feature)
|
||||
* [Issue Submission Guidelines](#submit)
|
||||
* [Pull Request Submission Guidelines](#submit-pr)
|
||||
* [Signing the CLA](#cla)
|
||||
|
||||
## <a name="coc"></a> Code of Conduct
|
||||
|
||||
Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc].
|
||||
|
||||
## <a name="requests"></a> Questions, Bugs, Features
|
||||
|
||||
### <a name="question"></a> Got a Question or Problem?
|
||||
|
||||
Do not open issues for general support questions as we want to keep GitHub issues for bug reports
|
||||
and feature requests. You've got much better chances of getting your question answered on dedicated
|
||||
support platforms, the best being [Stack Overflow][stackoverflow].
|
||||
|
||||
Stack Overflow is a much better place to ask questions since:
|
||||
|
||||
- there are thousands of people willing to help on Stack Overflow
|
||||
- questions and answers stay available for public viewing so your question / answer might help
|
||||
someone else
|
||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||
|
||||
To save your and our time, we will systematically close all issues that are requests for general
|
||||
support and redirect people to the section you are reading right now.
|
||||
|
||||
### <a name="issue"></a> Found an Issue or Bug?
|
||||
|
||||
If you find a bug in the source code, you can help us by submitting an issue to our
|
||||
[GitHub Repository][github]. Even better, you can submit a Pull Request with a fix.
|
||||
|
||||
### <a name="feature"></a> Missing a Feature?
|
||||
|
||||
You can request a new feature by submitting an issue to our [GitHub Repository][github-issues].
|
||||
|
||||
If you would like to implement a new feature then consider what kind of change it is:
|
||||
|
||||
* **Major Changes** that you wish to contribute to the project should be discussed first in an
|
||||
[GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature.
|
||||
* **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github]
|
||||
as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr).
|
||||
|
||||
## <a name="submit"></a> Issue Submission Guidelines
|
||||
|
||||
Before you submit your issue search the archive, maybe your question was already answered.
|
||||
|
||||
If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize
|
||||
the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
|
||||
|
||||
The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to
|
||||
make it easier to understand and categorize the issue.
|
||||
|
||||
## <a name="submit-pr"></a> Pull Request Submission Guidelines
|
||||
|
||||
By submitting a pull request for a code or doc contribution, you need to have the right
|
||||
to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla]
|
||||
for details.
|
||||
|
||||
Before you submit your pull request consider the following guidelines:
|
||||
|
||||
* Search [GitHub][github-pr] for an open or closed Pull Request
|
||||
that relates to your submission. You don't want to duplicate effort.
|
||||
* Make your changes in a new git branch:
|
||||
|
||||
```shell
|
||||
git checkout -b my-fix-branch master
|
||||
```
|
||||
* Test your changes and, if relevant, expand the automated test suite.
|
||||
* Create your patch commit, including appropriate test cases.
|
||||
* If the changes affect public APIs, change or add relevant documentation.
|
||||
* Commit your changes using a descriptive commit message.
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
|
||||
* Push your branch to GitHub:
|
||||
|
||||
```shell
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
* In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test.
|
||||
We will also notify you if you have not yet signed the [contribution agreement][cla].
|
||||
|
||||
* If you find that the continunous integration has failed, look into the logs to find out
|
||||
if your changes caused test failures, the commit message was malformed etc. If you find that the
|
||||
tests failed or times out for unrelated reasons, you can ping a team member so that the build can be
|
||||
restarted.
|
||||
|
||||
* If we suggest changes, then:
|
||||
|
||||
* Make the required updates.
|
||||
* Test your changes and test cases.
|
||||
* Commit your changes to your branch (e.g. `my-fix-branch`).
|
||||
* Push the changes to your GitHub repository (this will update your Pull Request).
|
||||
|
||||
You can also amend the initial commits and force push them to the branch.
|
||||
|
||||
```shell
|
||||
git rebase master -i
|
||||
git push origin my-fix-branch -f
|
||||
```
|
||||
|
||||
This is generally easier to follow, but separate commits are useful if the Pull Request contains
|
||||
iterations that might be interesting to see side-by-side.
|
||||
|
||||
That's it! Thank you for your contribution!
|
||||
|
||||
## <a name="cla"></a> Signing the Contributor License Agreement (CLA)
|
||||
|
||||
Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done
|
||||
so before. It's a quick process, we promise, and you will be able to do it all online
|
||||
|
||||
You can read [ProcessOne Contribution License Agreement][cla] in PDF.
|
||||
|
||||
This is part of the legal framework of the open-source ecosystem that adds some red tape,
|
||||
but protects both the contributor and the company / foundation behind the project. It also
|
||||
gives us the option to relicense the code with a more permissive license in the future.
|
||||
|
||||
|
||||
[coc]: https://github.com/FluuxIO/go-xmpp/blob/master/CODE_OF_CONDUCT.md
|
||||
[stackoverflow]: https://stackoverflow.com/
|
||||
[github]: https://github.com/FluuxIO/go-xmpp
|
||||
[github-issues]: https://github.com/FluuxIO/go-xmpp/issues
|
||||
[github-new-issue]: https://github.com/FluuxIO/go-xmpp/issues/new
|
||||
[github-pr]: https://github.com/FluuxIO/go-xmpp/pulls
|
||||
[cla]: https://www.process-one.net/resources/ejabberd-cla.pdf
|
||||
[license]: https://github.com/FluuxIO/go-xmpp/blob/master/LICENSE
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM golang:1.12
|
||||
WORKDIR /xmpp
|
||||
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
|
||||
COPY . ./
|
||||
@@ -1,27 +1,29 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2017, ProcessOne SARL
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
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 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
|
||||
* Redistributions of source code must retain the above copyright 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 the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 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.
|
||||
|
||||
@@ -1,6 +1,107 @@
|
||||
go-xmpp
|
||||
=======
|
||||
# Fluux XMPP
|
||||
|
||||
go xmpp library (original was written by russ cox )
|
||||
[](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)
|
||||
|
||||
[Documentation](https://godoc.org/github.com/xmppo/go-xmpp)
|
||||
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
||||
|
||||
The goal is to make simple to write simple XMPP clients and components:
|
||||
|
||||
- For automation (like for example monitoring of an XMPP service),
|
||||
- For building connected "things" by plugging them on an XMPP server,
|
||||
- For writing simple chatbot to control a service or a thing.
|
||||
- For writing XMPP servers components.
|
||||
|
||||
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/matterbridge/go-xmpp"
|
||||
"github.com/mattn/go-gtk/gtk"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gtk.Init(&os.Args)
|
||||
|
||||
window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL)
|
||||
window.SetTitle("GoTalk")
|
||||
window.Connect("destroy", func() {
|
||||
gtk.MainQuit()
|
||||
})
|
||||
vbox := gtk.NewVBox(false, 1)
|
||||
scrolledwin := gtk.NewScrolledWindow(nil, nil)
|
||||
textview := gtk.NewTextView()
|
||||
textview.SetEditable(false)
|
||||
textview.SetCursorVisible(false)
|
||||
scrolledwin.Add(textview)
|
||||
vbox.Add(scrolledwin)
|
||||
|
||||
buffer := textview.GetBuffer()
|
||||
|
||||
entry := gtk.NewEntry()
|
||||
vbox.PackEnd(entry, false, false, 0)
|
||||
|
||||
window.Add(vbox)
|
||||
window.SetSizeRequest(300, 400)
|
||||
window.ShowAll()
|
||||
|
||||
dialog := gtk.NewDialog()
|
||||
dialog.SetTitle(window.GetTitle())
|
||||
sgroup := gtk.NewSizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
|
||||
|
||||
hbox := gtk.NewHBox(false, 1)
|
||||
dialog.GetVBox().Add(hbox)
|
||||
label := gtk.NewLabel("username:")
|
||||
sgroup.AddWidget(label)
|
||||
hbox.Add(label)
|
||||
username := gtk.NewEntry()
|
||||
hbox.Add(username)
|
||||
|
||||
hbox = gtk.NewHBox(false, 1)
|
||||
dialog.GetVBox().Add(hbox)
|
||||
label = gtk.NewLabel("password:")
|
||||
sgroup.AddWidget(label)
|
||||
hbox.Add(label)
|
||||
password := gtk.NewEntry()
|
||||
password.SetVisibility(false)
|
||||
hbox.Add(password)
|
||||
|
||||
dialog.AddButton(gtk.STOCK_OK, gtk.RESPONSE_OK)
|
||||
dialog.AddButton(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
dialog.SetDefaultResponse(gtk.RESPONSE_OK)
|
||||
dialog.SetTransientFor(window)
|
||||
dialog.ShowAll()
|
||||
res := dialog.Run()
|
||||
username_ := username.GetText()
|
||||
password_ := password.GetText()
|
||||
dialog.Destroy()
|
||||
if res != gtk.RESPONSE_OK {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
xmpp.DefaultConfig = tls.Config{
|
||||
ServerName: "talk.google.com",
|
||||
InsecureSkipVerify: false,
|
||||
}
|
||||
|
||||
talk, err := xmpp.NewClient("talk.google.com:443", username_, password_, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
entry.Connect("activate", func() {
|
||||
text := entry.GetText()
|
||||
tokens := strings.SplitN(text, " ", 2)
|
||||
if len(tokens) == 2 {
|
||||
func() {
|
||||
defer recover()
|
||||
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
|
||||
entry.SetText("")
|
||||
}()
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
func() {
|
||||
defer recover()
|
||||
chat, err := talk.Recv()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var iter gtk.TextIter
|
||||
buffer.GetStartIter(&iter)
|
||||
if msg, ok := chat.(xmpp.Chat); ok {
|
||||
buffer.Insert(&iter, msg.Remote+": "+msg.Text+"\n")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
gtk.Main()
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/matterbridge/go-xmpp"
|
||||
)
|
||||
|
||||
var (
|
||||
server = flag.String("server", "talk.google.com:443", "server")
|
||||
username = flag.String("username", "", "username")
|
||||
password = flag.String("password", "", "password")
|
||||
status = flag.String("status", "xa", "status")
|
||||
statusMessage = flag.String("status-msg", "I for one welcome our new codebot overlords.", "status message")
|
||||
notls = flag.Bool("notls", false, "No TLS")
|
||||
debug = flag.Bool("debug", false, "debug output")
|
||||
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]})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Custom Stanza example
|
||||
|
||||
This module show how to implement a custom extension for your own client, without having to modify or fork Fluux XMPP.
|
||||
|
||||
It help integrating your custom extension in the standard stream parsing, marshalling and unmarshalling workflow.
|
||||
@@ -0,0 +1,49 @@
|
||||
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{})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Advanced component: delegation
|
||||
|
||||
`delegation` is an example of advanced component supporting Namespace Delegation
|
||||
([XEP-0355](https://xmpp.org/extensions/xep-0355.html)) and privileged entity
|
||||
([XEP-356](https://xmpp.org/extensions/xep-0356.html)).
|
||||
@@ -0,0 +1,223 @@
|
||||
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) {
|
||||
// Higher level discovery
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
module gosrc.io/xmpp/_examples
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.3.0 // indirect
|
||||
github.com/processone/mpg123 v1.0.0
|
||||
github.com/processone/soundcloud v1.0.0
|
||||
gosrc.io/xmpp v0.1.1
|
||||
)
|
||||
|
||||
replace gosrc.io/xmpp => ./../
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA=
|
||||
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
||||
github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE=
|
||||
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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=
|
||||
@@ -0,0 +1,22 @@
|
||||
# xmpp_component
|
||||
|
||||
This component will connect to ejabberd and act as a subdomain "service" of your primary XMPP domain
|
||||
(in that case localhost).
|
||||
|
||||
This component does nothing expect connect and show up in service discovery.
|
||||
|
||||
To be able to connect this component, you need to add a listener to your XMPP server.
|
||||
|
||||
Here is an example ejabberd configuration for that component listener:
|
||||
|
||||
```yaml
|
||||
listen:
|
||||
...
|
||||
-
|
||||
port: 8888
|
||||
module: ejabberd_service
|
||||
password: "mypass"
|
||||
```
|
||||
|
||||
ejabberd will listen for a component (service) on port 8888 and allows it to connect using the
|
||||
secret "mypass".
|
||||
@@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"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"})
|
||||
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
|
||||
_ = 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"})
|
||||
|
||||
var payload stanza.DiscoItems
|
||||
if discoItems.Node == "" {
|
||||
payload = stanza.DiscoItems{
|
||||
Items: []stanza.DiscoItem{
|
||||
{Name: "test node", JID: "service.localhost", Node: "node1"},
|
||||
},
|
||||
}
|
||||
}
|
||||
iqResp.Payload = &payload
|
||||
_ = 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"})
|
||||
var payload stanza.Version
|
||||
payload.Name = "Fluux XMPP Component"
|
||||
payload.Version = "0.0.1"
|
||||
iq.Payload = &payload
|
||||
_ = c.Send(iqResp)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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,
|
||||
}
|
||||
|
||||
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 ?)
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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
|
||||
@@ -0,0 +1,53 @@
|
||||
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
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Interesting reference on backoff:
|
||||
- Exponential Backoff And Jitter (AWS Blog):
|
||||
https://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||
|
||||
We use Jitter as a default for exponential backoff, as the goal of
|
||||
this module is not to provide precise 'ticks', but good behaviour to
|
||||
implement retries that are helping the server to recover faster in
|
||||
case of congestion.
|
||||
|
||||
It can be used in several ways:
|
||||
- Using duration to get next sleep time.
|
||||
- Using ticker channel to trigger callback function on tick
|
||||
|
||||
The functions for Backoff are not threadsafe, but you can:
|
||||
- Keep the attempt counter on your end and use durationForAttempt(int)
|
||||
- Use lock in your own code to protect the Backoff structure.
|
||||
|
||||
TODO: Implement Backoff Ticker channel
|
||||
TODO: Implement throttler interface. Throttler could be used to implement various reconnect strategies.
|
||||
*/
|
||||
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBase int = 20 // Backoff base, in ms
|
||||
defaultFactor int = 2
|
||||
defaultCap int = 180000 // 3 minutes
|
||||
)
|
||||
|
||||
// backoff provides increasing duration with the number of attempt
|
||||
// performed. The structure is used to support exponential backoff on
|
||||
// connection attempts to avoid hammering the server we are connecting
|
||||
// to.
|
||||
type backoff struct {
|
||||
NoJitter bool
|
||||
Base int
|
||||
Factor int
|
||||
Cap int
|
||||
lastDuration int
|
||||
attempt int
|
||||
}
|
||||
|
||||
// duration returns the duration to apply to the current attempt.
|
||||
func (b *backoff) duration() time.Duration {
|
||||
d := b.durationForAttempt(b.attempt)
|
||||
b.attempt++
|
||||
return d
|
||||
}
|
||||
|
||||
// wait sleeps for backoff duration for current attempt.
|
||||
func (b *backoff) wait() {
|
||||
time.Sleep(b.duration())
|
||||
}
|
||||
|
||||
// durationForAttempt returns a duration for an attempt number, in a stateless way.
|
||||
func (b *backoff) durationForAttempt(attempt int) time.Duration {
|
||||
b.setDefault()
|
||||
expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt)))
|
||||
d := int(math.Trunc(expBackoff))
|
||||
if !b.NoJitter {
|
||||
d = rand.Intn(d)
|
||||
}
|
||||
return time.Duration(d) * time.Millisecond
|
||||
}
|
||||
|
||||
// reset sets back the number of attempts to 0. This is to be called after a successful operation has been performed,
|
||||
// to reset the exponential backoff interval.
|
||||
func (b *backoff) reset() {
|
||||
b.attempt = 0
|
||||
}
|
||||
|
||||
func (b *backoff) setDefault() {
|
||||
if b.Base == 0 {
|
||||
b.Base = defaultBase
|
||||
}
|
||||
|
||||
if b.Cap == 0 {
|
||||
b.Cap = defaultCap
|
||||
}
|
||||
|
||||
if b.Factor == 0 {
|
||||
b.Factor = defaultFactor
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
We use full jitter as default for now as it seems to provide good behaviour for reconnect.
|
||||
|
||||
Base is the default interval between attempts (if backoff Factor was equal to 1)
|
||||
|
||||
Attempt is the number of retry for operation. If we start attempt at 0, first sleep equals base.
|
||||
|
||||
Cap is the maximum sleep time duration we tolerate between attempts
|
||||
*/
|
||||
@@ -0,0 +1,22 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDurationForAttempt_NoJitter(t *testing.T) {
|
||||
b := backoff{Base: 25, NoJitter: true}
|
||||
bInMS := time.Duration(b.Base) * time.Millisecond
|
||||
if b.durationForAttempt(0) != bInMS {
|
||||
t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.durationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond)
|
||||
}
|
||||
var prevDuration, d time.Duration
|
||||
for i := 0; i < 10; i++ {
|
||||
d = b.durationForAttempt(i)
|
||||
if !(d >= prevDuration) {
|
||||
t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration)
|
||||
}
|
||||
prevDuration = d
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
}
|
||||
|
||||
stanza.DefaultTlsConfig.ServerName = c.domain
|
||||
tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig)
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
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
|
||||
}
|
||||
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
@@ -0,0 +1,281 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# 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
|
||||
|
||||
If you server is on standard port and XMPP domains matches the hostname you can simply use:
|
||||
|
||||
```
|
||||
$ xmpp-check myhost.net
|
||||
2019/05/16 16:04:36 All checks passed
|
||||
```
|
||||
|
||||
You can also pass the port and the XMPP domain if different from the server hostname:
|
||||
|
||||
```
|
||||
$ xmpp-check myhost.net:5222 xmppdomain.net
|
||||
2019/05/16 16:05:21 All checks passed
|
||||
```
|
||||
|
||||
Error code will be non-zero in case of error. You can thus use it directly with your usual
|
||||
monitoring scripts.
|
||||
@@ -0,0 +1,3 @@
|
||||
# TODO
|
||||
|
||||
- Use a config file to define the checks to perform as client on an XMPP server.
|
||||
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
|
||||
xmpp-check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires.
|
||||
|
||||
*/
|
||||
package main
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
|
||||
if len(args) == 0 {
|
||||
log.Fatal("usage: xmpp-check host[:port] [domain]")
|
||||
}
|
||||
|
||||
var address string
|
||||
var domain string
|
||||
if len(args) >= 1 {
|
||||
address = args[0]
|
||||
}
|
||||
|
||||
if len(args) >= 2 {
|
||||
domain = args[1]
|
||||
}
|
||||
|
||||
runCheck(address, domain)
|
||||
}
|
||||
|
||||
func runCheck(address, domain string) {
|
||||
client, err := xmpp.NewChecker(address, domain)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Error: ", err)
|
||||
}
|
||||
|
||||
if err = client.Check(); err != nil {
|
||||
log.Fatal("Failed connection check: ", err)
|
||||
}
|
||||
|
||||
log.Println("All checks passed")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
comment: off
|
||||
@@ -0,0 +1,5 @@
|
||||
build:
|
||||
build:
|
||||
image: fluux/build
|
||||
dockerfile: Dockerfile
|
||||
encrypted_env_file: codeship.env.encrypted
|
||||
@@ -0,0 +1,5 @@
|
||||
- type: serial
|
||||
steps:
|
||||
- name: test
|
||||
service: build
|
||||
command: ./test.sh
|
||||
@@ -0,0 +1 @@
|
||||
yVKgVFeKW6SSnC/KgLYpfYtTcqqTke1gOIW5GUiVvRijnhweOJiYKFPmwPjpt1FVrg4WVELQUNbxn3lmfyHVVF7r
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
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
|
||||
*/
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"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
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type ConnError struct {
|
||||
frame xerrors.Frame
|
||||
err error
|
||||
// Permanent will be true if error is not recoverable
|
||||
Permanent bool
|
||||
}
|
||||
|
||||
func NewConnError(err error, permanent bool) ConnError {
|
||||
return ConnError{err: err, frame: xerrors.Caller(1), Permanent: permanent}
|
||||
}
|
||||
|
||||
func (e ConnError) Format(s fmt.State, verb rune) {
|
||||
xerrors.FormatError(e, s, verb)
|
||||
}
|
||||
|
||||
func (e ConnError) FormatError(p xerrors.Printer) error {
|
||||
e.frame.Format(p)
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e ConnError) Error() string {
|
||||
return fmt.Sprint(e)
|
||||
}
|
||||
|
||||
func (e ConnError) Unwrap() error { return e.err }
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Fluux XMPP is an modern and full-featured XMPP library that can be used to build clients or
|
||||
server components.
|
||||
|
||||
The goal is to make simple to write modern compliant XMPP software:
|
||||
|
||||
- For automation (like for example monitoring of an XMPP service),
|
||||
- For building connected "things" by plugging them on an XMPP server,
|
||||
- For writing simple chatbots to control a service or a thing.
|
||||
- For writing XMPP servers components. Fluux XMPP supports:
|
||||
- XEP-0114: Jabber Component Protocol
|
||||
- XEP-0355: Namespace Delegation
|
||||
- XEP-0356: Privileged Entity
|
||||
|
||||
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
|
||||
|
||||
The library includes a StreamManager that provides features like autoreconnect exponential back-off.
|
||||
|
||||
The library is implementing latest versions of the XMPP specifications (RFC 6120 and RFC 6121), and includes
|
||||
support for many extensions.
|
||||
|
||||
Clients
|
||||
|
||||
Fluux XMPP can be use to create fully interactive XMPP clients (for
|
||||
example console-based), but it is more commonly used to build automated
|
||||
clients (connected devices, automation scripts, chatbots, etc.).
|
||||
|
||||
Components
|
||||
|
||||
XMPP components can typically be used to extends the features of an XMPP
|
||||
server, in a portable way, using component protocol over persistent TCP
|
||||
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 +1,8 @@
|
||||
module github.com/matterbridge/go-xmpp
|
||||
module gosrc.io/xmpp
|
||||
|
||||
go 1.21.5
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/net v0.25.0
|
||||
github.com/google/go-cmp v0.2.0
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
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=
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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
@@ -0,0 +1,86 @@
|
||||
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
@@ -0,0 +1,32 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ensurePort adds a port to an address if none are provided.
|
||||
// It handles both IPV4 and IPV6 addresses.
|
||||
func ensurePort(addr string, port int) string {
|
||||
// This is an IPV6 address literal
|
||||
if strings.HasPrefix(addr, "[") {
|
||||
// if address has no port (behind his ipv6 address) - add default port
|
||||
if strings.LastIndex(addr, ":") <= strings.LastIndex(addr, "]") {
|
||||
return addr + ":" + strconv.Itoa(port)
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// This is either an IPV6 address without bracket or an IPV4 address
|
||||
switch strings.Count(addr, ":") {
|
||||
case 0:
|
||||
// This is IPV4 without port
|
||||
return addr + ":" + strconv.Itoa(port)
|
||||
case 1:
|
||||
// This is 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
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
@@ -0,0 +1,252 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
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)
|
||||
if s.TlsEnabled {
|
||||
s.reset(conn, tlsConn, o)
|
||||
}
|
||||
|
||||
if !s.TlsEnabled && !o.Insecure {
|
||||
return nil, nil, NewConnError(errors.New("failed to negotiate TLS session"), true)
|
||||
}
|
||||
|
||||
// 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) 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
|
||||
}
|
||||
s.TlsEnabled = true
|
||||
|
||||
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
|
||||
stanza.DefaultTlsConfig.ServerName = domain
|
||||
tlsConn := tls.Client(conn, &stanza.DefaultTlsConfig)
|
||||
// We convert existing connection to TLS
|
||||
if s.err = tlsConn.Handshake(); s.err != nil {
|
||||
return tlsConn
|
||||
}
|
||||
|
||||
// We check that cert matches hostname
|
||||
s.err = tlsConn.VerifyHostname(domain)
|
||||
return tlsConn
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
# XMPP Stanza
|
||||
|
||||
XMPP `stanza` package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
||||
|
||||
## Stanza creation
|
||||
|
||||
When creating stanzas, you can use two approaches:
|
||||
|
||||
1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the
|
||||
stanza.
|
||||
2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the
|
||||
final stanza.
|
||||
|
||||
The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of
|
||||
struct that you can build by hand.
|
||||
|
||||
### Composing stanzas manually with structs
|
||||
|
||||
Here is for example how you would generate an IQ discovery result:
|
||||
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
identity := stanza.Identity{
|
||||
Name: opts.Name,
|
||||
Category: opts.Category,
|
||||
Type: opts.Type,
|
||||
}
|
||||
payload := stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
Identity: []stanza.Identity{identity},
|
||||
Features: []stanza.Feature{
|
||||
{Var: stanza.NSDiscoInfo},
|
||||
{Var: stanza.NSDiscoItems},
|
||||
{Var: "jabber:iq:version"},
|
||||
{Var: "urn:xmpp:delegation:1"},
|
||||
},
|
||||
}
|
||||
iqResp.Payload = &payload
|
||||
|
||||
### Using helpers
|
||||
|
||||
Here is for example how you would generate an IQ discovery result using Builder:
|
||||
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||
disco := iq.DiscoInfo()
|
||||
disco.AddIdentity("Test Component", "gateway", "service")
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
|
||||
## Payload and extensions
|
||||
|
||||
### Message
|
||||
|
||||
Here is the list of implemented message extensions:
|
||||
|
||||
- `Delegation`
|
||||
|
||||
- `Markable`
|
||||
- `MarkAcknowledged`
|
||||
- `MarkDisplayed`
|
||||
- `MarkReceived`
|
||||
|
||||
- `StateActive`
|
||||
- `StateComposing`
|
||||
- `StateGone`
|
||||
- `StateInactive`
|
||||
- `StatePaused`
|
||||
|
||||
- `HTML`
|
||||
|
||||
- `OOB`
|
||||
|
||||
- `ReceiptReceived`
|
||||
- `ReceiptRequest`
|
||||
|
||||
- `Mood`
|
||||
|
||||
### Presence
|
||||
|
||||
Here is the list of implemented presence extensions:
|
||||
|
||||
- `MucPresence`
|
||||
|
||||
### IQ
|
||||
|
||||
IQ (Information Queries) contain a payload associated with the request and possibly an error. The main difference with
|
||||
Message and Presence extension is that you can only have one payload per IQ. The XMPP specification does not support
|
||||
having multiple payloads.
|
||||
|
||||
Here is the list of structs implementing IQPayloads:
|
||||
|
||||
- `ControlSet`
|
||||
- `ControlSetResponse`
|
||||
- `Delegation`
|
||||
- `DiscoInfo`
|
||||
- `DiscoItems`
|
||||
- `Pubsub`
|
||||
- `Version`
|
||||
- `Node`
|
||||
|
||||
Finally, when the payload of the parsed stanza is unknown, the parser will provide the unknown payload as a generic
|
||||
`Node` element. You can also use the Node struct to add custom information on stanza generation. However, in both cases,
|
||||
you may also consider [adding your own custom extensions on stanzas]().
|
||||
|
||||
|
||||
## Adding your own custom extensions on stanzas
|
||||
|
||||
Extensions are registered on launch using the `Registry`. It can be used to register you own custom payload. You may
|
||||
want to do so to support extensions we did not yet implement, or to add your own custom extensions to your XMPP stanzas.
|
||||
|
||||
To create an extension you need:
|
||||
1. to create a struct for that extension. It need to have XMLName for consistency and to tagged at the struct level with
|
||||
`xml` info.
|
||||
2. It need to implement one or several extensions interface: stanza.IQPayload, stanza.MsgExtension and / or
|
||||
stanza.PresExtension
|
||||
3. Add that custom extension to the stanza.TypeRegistry during the file init.
|
||||
|
||||
Here an example code showing how to create a custom IQPayload.
|
||||
|
||||
```go
|
||||
package myclient
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
type CustomPayload struct {
|
||||
XMLName xml.Name `xml:"my:custom:payload query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (c CustomPayload) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
func init() {
|
||||
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
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{})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// We should be able to properly parse delegation confirmation messages
|
||||
func TestParsingDelegationMessage(t *testing.T) {
|
||||
packetStr := `<message to='service.localhost' from='localhost'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<delegated namespace='http://jabber.org/protocol/pubsub'/>
|
||||
</delegation>
|
||||
</message>`
|
||||
var msg Message
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &msg); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as MsgExtension
|
||||
var nsDelegated string
|
||||
for _, ext := range msg.Extensions {
|
||||
if delegation, ok := ext.(*Delegation); ok {
|
||||
nsDelegated = delegation.Delegated.Namespace
|
||||
}
|
||||
}
|
||||
if nsDelegated != "http://jabber.org/protocol/pubsub" {
|
||||
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we can parse a delegation IQ.
|
||||
// The most important thing is to be able to
|
||||
func TestParsingDelegationIQ(t *testing.T) {
|
||||
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
|
||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||
<publish node='http://jabber.org/protocol/mood'>
|
||||
<item id='current'>
|
||||
<mood xmlns='http://jabber.org/protocol/mood'>
|
||||
<excited/>
|
||||
</mood>
|
||||
</item>
|
||||
</publish>
|
||||
</pubsub>
|
||||
</iq>
|
||||
</forwarded>
|
||||
</delegation>
|
||||
</iq>`
|
||||
var iq IQ
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &iq); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as IQPayload
|
||||
var node string
|
||||
if iq.Payload != nil {
|
||||
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||
packet := delegation.Forwarded.Stanza
|
||||
forwardedIQ, ok := packet.(IQ)
|
||||
if !ok {
|
||||
t.Errorf("Could not extract packet IQ")
|
||||
return
|
||||
}
|
||||
if forwardedIQ.Payload != nil {
|
||||
if pubsub, ok := forwardedIQ.Payload.(*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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
XMPP stanza package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
||||
*/
|
||||
package stanza
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
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})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package stanza
|
||||
|
||||
// ErrorType is a Enum of error attribute type
|
||||
type ErrorType string
|
||||
|
||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
||||
const (
|
||||
ErrorTypeAuth ErrorType = "auth"
|
||||
ErrorTypeCancel ErrorType = "cancel"
|
||||
ErrorTypeContinue ErrorType = "continue"
|
||||
ErrorTypeModify ErrorType = "modify"
|
||||
ErrorTypeWait ErrorType = "wait"
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
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{})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestControlSet(t *testing.T) {
|
||||
packet := `
|
||||
<iq to='test@localhost/jukebox' from='admin@localhost/mbp' type='set' id='2'>
|
||||
<set xmlns='urn:xmpp:iot:control' xml:lang='en'>
|
||||
<string name='action' value='play'/>
|
||||
<string name='url' value='https://soundcloud.com/radiohead/spectre'/>
|
||||
</set>
|
||||
</iq>`
|
||||
|
||||
parsedIQ := IQ{}
|
||||
data := []byte(packet)
|
||||
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
|
||||
t.Errorf("Paylod is not an iot control set: %v", cs)
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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) {
|
||||
d.Node = node
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
|
||||
d.Identity = ident
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
|
||||
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
|
||||
}
|
||||
|
||||
type DiscoItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
JID string `xml:"jid,attr,omitempty"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestDiscoInfoBuilder(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")
|
||||
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshall
|
||||
var parsedIQ stanza.IQ
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
|
||||
}
|
||||
|
||||
// Check result
|
||||
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
|
||||
if !ok {
|
||||
t.Errorf("Parsed stanza does not contain an 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Message Packet
|
||||
|
||||
// Message implements RFC 6120 - A.5 Client Namespace (a part)
|
||||
type Message struct {
|
||||
XMLName xml.Name `xml:"message"`
|
||||
Attrs
|
||||
|
||||
Subject string `xml:"subject,omitempty"`
|
||||
Body string `xml:"body,omitempty"`
|
||||
Thread string `xml:"thread,omitempty"`
|
||||
Error Err `xml:"error,omitempty"`
|
||||
Extensions []MsgExtension `xml:",omitempty"`
|
||||
}
|
||||
|
||||
func (Message) Name() string {
|
||||
return "message"
|
||||
}
|
||||
|
||||
func NewMessage(a Attrs) Message {
|
||||
return Message{
|
||||
XMLName: xml.Name{Local: "message"},
|
||||
Attrs: a,
|
||||
}
|
||||
}
|
||||
|
||||
// Get search and extracts a specific extension on a message.
|
||||
// It receives a pointer to an MsgExtension. It will panic if the caller
|
||||
// does not pass a pointer.
|
||||
// It will return true if the passed extension is found and set the pointer
|
||||
// to the extension passed as parameter to the found extension.
|
||||
// It will return false if the extension is not found on the message.
|
||||
//
|
||||
// Example usage:
|
||||
// var oob xmpp.OOB
|
||||
// if ok := msg.Get(&oob); ok {
|
||||
// // oob extension has been found
|
||||
// }
|
||||
func (msg *Message) Get(ext MsgExtension) bool {
|
||||
target := reflect.ValueOf(ext)
|
||||
if target.Kind() != reflect.Ptr {
|
||||
panic("you must pass a pointer to the message Get method")
|
||||
}
|
||||
|
||||
for _, e := range msg.Extensions {
|
||||
if reflect.TypeOf(e) == target.Type() {
|
||||
source := reflect.ValueOf(e)
|
||||
if source.Kind() != reflect.Ptr {
|
||||
source = source.Elem()
|
||||
}
|
||||
target.Elem().Set(source.Elem())
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type messageDecoder struct{}
|
||||
|
||||
var message messageDecoder
|
||||
|
||||
func (messageDecoder) decode(p *xml.Decoder, se xml.StartElement) (Message, error) {
|
||||
var packet Message
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// XMPPFormat with all Extensions
|
||||
func (msg *Message) XMPPFormat() string {
|
||||
out, err := xml.MarshalIndent(msg, "", "")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for messages
|
||||
func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
msg.XMLName = start.Name
|
||||
|
||||
// Extract packet attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "id" {
|
||||
msg.Id = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "type" {
|
||||
msg.Type = StanzaType(attr.Value)
|
||||
}
|
||||
if attr.Name.Local == "to" {
|
||||
msg.To = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "from" {
|
||||
msg.From = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "lang" {
|
||||
msg.Lang = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
if msgExt := TypeRegistry.GetMsgExtension(tt.Name); msgExt != nil {
|
||||
// Decode message extension
|
||||
err = d.DecodeElement(msgExt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Extensions = append(msg.Extensions, msgExt)
|
||||
} else {
|
||||
// Decode standard message sub-elements
|
||||
var err error
|
||||
switch tt.Name.Local {
|
||||
case "body":
|
||||
err = d.DecodeElement(&msg.Body, &tt)
|
||||
case "thread":
|
||||
err = d.DecodeElement(&msg.Thread, &tt)
|
||||
case "subject":
|
||||
err = d.DecodeElement(&msg.Subject, &tt)
|
||||
case "error":
|
||||
err = d.DecodeElement(&msg.Error, &tt)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestGenerateMessage(t *testing.T) {
|
||||
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
message.Body = "Hi"
|
||||
message.Subject = "Msg Subject"
|
||||
|
||||
data, err := xml.Marshal(message)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
parsedMessage := stanza.Message{}
|
||||
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if !xmlEqual(parsedMessage, message) {
|
||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedMessage, message))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeError(t *testing.T) {
|
||||
str := `<message from='juliet@capulet.com'
|
||||
id='msg_1'
|
||||
to='romeo@montague.lit'
|
||||
type='error'>
|
||||
<error type='cancel'>
|
||||
<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
</error>
|
||||
</message>`
|
||||
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message error stanza unmarshall error: %v", err)
|
||||
return
|
||||
}
|
||||
if parsedMessage.Error.Type != "cancel" {
|
||||
t.Errorf("incorrect error type: %s", parsedMessage.Error.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOOB(t *testing.T) {
|
||||
image := "https://localhost/image.png"
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||
ext := stanza.OOB{
|
||||
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
|
||||
URL: image,
|
||||
}
|
||||
msg.Extensions = append(msg.Extensions, &ext)
|
||||
|
||||
// OOB can properly be found
|
||||
var oob stanza.OOB
|
||||
// Try to find and
|
||||
if ok := msg.Get(&oob); !ok {
|
||||
t.Error("could not find oob extension")
|
||||
return
|
||||
}
|
||||
if oob.URL != image {
|
||||
t.Errorf("OOB URL was not properly extracted: ''%s", oob.URL)
|
||||
}
|
||||
|
||||
// Markable is not found
|
||||
var m stanza.Markable
|
||||
if ok := msg.Get(&m); ok {
|
||||
t.Error("we should not have found markable extension")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0333 - Chat Markers: https://xmpp.org/extensions/xep-0333.html
|
||||
*/
|
||||
|
||||
const NSMsgChatMarkers = "urn:xmpp:chat-markers:0"
|
||||
|
||||
type Markable struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 markable"`
|
||||
}
|
||||
|
||||
type MarkReceived struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 received"`
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
type MarkDisplayed struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 displayed"`
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
type MarkAcknowledged struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 acknowledged"`
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{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{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0085 - Chat State Notifications: https://xmpp.org/extensions/xep-0085.html
|
||||
*/
|
||||
|
||||
const NSMsgChatStateNotifications = "http://jabber.org/protocol/chatstates"
|
||||
|
||||
type StateActive struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates active"`
|
||||
}
|
||||
|
||||
type StateComposing struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates composing"`
|
||||
}
|
||||
|
||||
type StateGone struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates gone"`
|
||||
}
|
||||
|
||||
type StateInactive struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates inactive"`
|
||||
}
|
||||
|
||||
type StatePaused struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates paused"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{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{})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type HTML struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/xhtml-im html"`
|
||||
Body HTMLBody
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
}
|
||||
|
||||
type HTMLBody struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/1999/xhtml body"`
|
||||
// InnerXML MUST be valid xhtml. We do not check if it is valid when generating the XMPP stanza.
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"http://jabber.org/protocol/xhtml-im", "html"}, HTML{})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestHTMLGen(t *testing.T) {
|
||||
htmlBody := "<p>Hello <b>World</b></p>"
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||
msg.Body = "Hello World"
|
||||
body := stanza.HTMLBody{
|
||||
InnerXML: htmlBody,
|
||||
}
|
||||
html := stanza.HTML{Body: body}
|
||||
msg.Extensions = append(msg.Extensions, html)
|
||||
|
||||
result := msg.XMPPFormat()
|
||||
str := `<message to="test@localhost"><body>Hello World</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>Hello <b>World</b></p></body></html></message>`
|
||||
if result != str {
|
||||
t.Errorf("incorrect serialize message:\n%s", result)
|
||||
}
|
||||
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message HTML unmarshall error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedMessage.Body != msg.Body {
|
||||
t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body)
|
||||
}
|
||||
|
||||
var h stanza.HTML
|
||||
if ok := parsedMessage.Get(&h); !ok {
|
||||
t.Error("could not extract HTML body")
|
||||
}
|
||||
|
||||
if h.Body.InnerXML != htmlBody {
|
||||
t.Errorf("could not extract html body: '%s'", h.Body.InnerXML)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0066 - Out of Band Data: https://xmpp.org/extensions/xep-0066.html
|
||||
*/
|
||||
|
||||
type OOB struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"jabber:x:oob x"`
|
||||
URL string `xml:"url"`
|
||||
Desc string `xml:"desc,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"jabber:x:oob", "x"}, OOB{})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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{})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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"
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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"
|
||||
)
|
||||
@@ -0,0 +1,151 @@
|
||||
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 + "/>")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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"`
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
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{})
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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"
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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{})
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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{})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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
|
||||
@@ -0,0 +1,17 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
var DefaultTlsConfig tls.Config
|
||||
|
||||
// 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"`
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// Compare iq structure but ignore empty namespace as they are set properly on
|
||||
// marshal / unmarshal. There is no need to manage them on the manually
|
||||
// crafted structure.
|
||||
func xmlEqual(x, y interface{}) bool {
|
||||
return cmp.Equal(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
// xmlDiff compares xml structures ignoring namespace preferences
|
||||
func xmlDiff(x, y interface{}) string {
|
||||
return cmp.Diff(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
func xmlOpts() cmp.Options {
|
||||
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
|
||||
opts := cmp.Options{
|
||||
cmp.FilterValues(func(x, y interface{}) bool {
|
||||
xx, xok := x.(xml.Name)
|
||||
yy, yok := y.(xml.Name)
|
||||
if xok && yok {
|
||||
zero := xml.Name{}
|
||||
if xx == zero || yy == zero {
|
||||
return true
|
||||
}
|
||||
if xx.Space == "" || yy.Space == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, alwaysEqual),
|
||||
}
|
||||
return opts
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Mediated Read / Write on socket
|
||||
// Used if logFile from Config is not nil
|
||||
type streamLogger struct {
|
||||
socket io.ReadWriter // Actual connection
|
||||
logFile *os.File
|
||||
}
|
||||
|
||||
func newStreamLogger(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
|
||||
if logFile == nil {
|
||||
return conn
|
||||
} else {
|
||||
return &streamLogger{conn, logFile}
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *streamLogger) Read(p []byte) (n int, err error) {
|
||||
n, err = sp.socket.Read(p)
|
||||
if n > 0 {
|
||||
sp.logFile.Write([]byte("RECV:\n")) // Prefix
|
||||
if n, err := sp.logFile.Write(p[:n]); err != nil {
|
||||
return n, err
|
||||
}
|
||||
sp.logFile.Write([]byte("\n\n")) // Separator
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (sp *streamLogger) Write(p []byte) (n int, err error) {
|
||||
sp.logFile.Write([]byte("SEND:\n")) // Prefix
|
||||
for _, w := range []io.Writer{sp.socket, sp.logFile} {
|
||||
n, err = w.Write(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n != len(p) {
|
||||
err = io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
}
|
||||
sp.logFile.Write([]byte("\n\n")) // Separator
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Make RECV, SEND prefixes +
|
||||
*/
|
||||
@@ -0,0 +1,168 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// The Fluux XMPP lib can manage client or component XMPP streams.
|
||||
// The StreamManager handles the stream workflow handling the common
|
||||
// stream events and doing the right operations.
|
||||
//
|
||||
// It can handle:
|
||||
// - Client
|
||||
// - Stream establishment workflow
|
||||
// - Reconnection strategies, with exponential backoff. It also takes into account
|
||||
// permanent errors to avoid useless reconnection loops.
|
||||
// - Metrics processing
|
||||
|
||||
// StreamClient is an interface used by StreamManager to control Client lifecycle,
|
||||
// set callback and trigger reconnection.
|
||||
type StreamClient interface {
|
||||
Connect() error
|
||||
Send(packet stanza.Packet) error
|
||||
SendRaw(packet string) error
|
||||
Disconnect()
|
||||
SetHandler(handler EventHandler)
|
||||
}
|
||||
|
||||
// Sender is an interface provided by Stream clients to allow sending XMPP data.
|
||||
// It is mostly use in callback to pass a limited subset of the stream client interface
|
||||
type Sender interface {
|
||||
Send(packet stanza.Packet) error
|
||||
SendRaw(packet string) error
|
||||
}
|
||||
|
||||
// StreamManager supervises an XMPP client connection. Its role is to handle connection events and
|
||||
// apply reconnection strategy.
|
||||
type StreamManager struct {
|
||||
client StreamClient
|
||||
PostConnect PostConnect
|
||||
|
||||
// Store low level metrics
|
||||
Metrics *Metrics
|
||||
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type PostConnect func(c Sender)
|
||||
|
||||
// NewStreamManager creates a new StreamManager structure, intended to support
|
||||
// handling XMPP client state event changes and auto-trigger reconnection
|
||||
// based on StreamManager configuration.
|
||||
// TODO: Move parameters to Start and remove factory method
|
||||
func NewStreamManager(client StreamClient, pc PostConnect) *StreamManager {
|
||||
return &StreamManager{
|
||||
client: client,
|
||||
PostConnect: pc,
|
||||
}
|
||||
}
|
||||
|
||||
// Run launches the connection of the underlying client or component
|
||||
// and wait until Disconnect is called, or for the manager to terminate due
|
||||
// to an unrecoverable error.
|
||||
func (sm *StreamManager) Run() error {
|
||||
if sm.client == nil {
|
||||
return errors.New("missing stream client")
|
||||
}
|
||||
|
||||
handler := func(e Event) {
|
||||
switch e.State {
|
||||
case StateConnected:
|
||||
sm.Metrics.setConnectTime()
|
||||
case StateSessionEstablished:
|
||||
sm.Metrics.setLoginTime()
|
||||
case StateDisconnected:
|
||||
// Reconnect on disconnection
|
||||
sm.connect()
|
||||
case StateStreamError:
|
||||
sm.client.Disconnect()
|
||||
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
|
||||
if e.StreamError != "conflict" {
|
||||
sm.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
sm.client.SetHandler(handler)
|
||||
|
||||
sm.wg.Add(1)
|
||||
if err := sm.connect(); err != nil {
|
||||
sm.wg.Done()
|
||||
return err
|
||||
}
|
||||
sm.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop cancels pending operations and terminates existing XMPP client.
|
||||
func (sm *StreamManager) Stop() {
|
||||
// Remove on disconnect handler to avoid triggering reconnect
|
||||
sm.client.SetHandler(nil)
|
||||
sm.client.Disconnect()
|
||||
sm.wg.Done()
|
||||
}
|
||||
|
||||
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
||||
func (sm *StreamManager) connect() error {
|
||||
var backoff backoff // TODO: Group backoff calculation features with connection manager?
|
||||
|
||||
for {
|
||||
var err error
|
||||
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
||||
sm.Metrics = initMetrics()
|
||||
|
||||
if err = sm.client.Connect(); err != nil {
|
||||
var actualErr ConnError
|
||||
if xerrors.As(err, &actualErr) {
|
||||
if actualErr.Permanent {
|
||||
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
|
||||
}
|
||||
}
|
||||
backoff.wait()
|
||||
} else { // We are connected, we can leave the retry loop
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sm.PostConnect != nil {
|
||||
sm.PostConnect(sm.client)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stream Metrics
|
||||
// ============================================================================
|
||||
|
||||
type Metrics struct {
|
||||
startTime time.Time
|
||||
// ConnectTime returns the duration between client initiation of the TCP/IP
|
||||
// connection to the server and actual TCP/IP session establishment.
|
||||
// This time includes DNS resolution and can be slightly higher if the DNS
|
||||
// resolution result was not in cache.
|
||||
ConnectTime time.Duration
|
||||
// LoginTime returns the between client initiation of the TCP/IP
|
||||
// connection to the server and the return of the login result.
|
||||
// This includes ConnectTime, but also XMPP level protocol negociation
|
||||
// like starttls.
|
||||
LoginTime time.Duration
|
||||
}
|
||||
|
||||
// initMetrics set metrics with default value and define the starting point
|
||||
// for duration calculation (connect time, login time, etc).
|
||||
func initMetrics() *Metrics {
|
||||
return &Metrics{
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) setConnectTime() {
|
||||
m.ConnectTime = time.Since(m.startTime)
|
||||
}
|
||||
|
||||
func (m *Metrics) setLoginTime() {
|
||||
m.LoginTime = time.Since(m.startTime)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//=============================================================================
|
||||
// TCP Server Mock
|
||||
|
||||
// ClientHandler is passed by the test client to provide custom behaviour to
|
||||
// the TCP server mock. This allows customizing the server behaviour to allow
|
||||
// testing clients under various scenarii.
|
||||
type ClientHandler func(t *testing.T, conn net.Conn)
|
||||
|
||||
// ServerMock is a simple TCP server that can be use to mock basic server
|
||||
// behaviour to test clients.
|
||||
type ServerMock struct {
|
||||
t *testing.T
|
||||
handler ClientHandler
|
||||
listener net.Listener
|
||||
connections []net.Conn
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Start launches the mock TCP server, listening to an actual address / port.
|
||||
func (mock *ServerMock) Start(t *testing.T, addr string, handler ClientHandler) {
|
||||
mock.t = t
|
||||
mock.handler = handler
|
||||
if err := mock.init(addr); err != nil {
|
||||
return
|
||||
}
|
||||
go mock.loop()
|
||||
}
|
||||
|
||||
func (mock *ServerMock) Stop() {
|
||||
close(mock.done)
|
||||
if mock.listener != nil {
|
||||
mock.listener.Close()
|
||||
}
|
||||
// Close all existing connections
|
||||
for _, c := range mock.connections {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Mock Server internals
|
||||
|
||||
// init starts listener on the provided address.
|
||||
func (mock *ServerMock) init(addr string) error {
|
||||
mock.done = make(chan struct{})
|
||||
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
mock.t.Errorf("TCPServerMock cannot listen on address: %q", addr)
|
||||
return err
|
||||
}
|
||||
mock.listener = l
|
||||
return nil
|
||||
}
|
||||
|
||||
// loop accepts connections and creates a go routine per connection.
|
||||
// The go routine is running the client handler, that is used to provide the
|
||||
// real TCP server behaviour.
|
||||
func (mock *ServerMock) loop() {
|
||||
listener := mock.listener
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-mock.done:
|
||||
return
|
||||
default:
|
||||
mock.t.Error("TCPServerMock accept error:", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
mock.connections = append(mock.connections, conn)
|
||||
// TODO Create and pass a context to cancel the handler if they are still around = avoid possible leak on complex handlers
|
||||
go mock.handler(mock.t, conn)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
export GO111MODULE=on
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -race -coverprofile=profile.out -covermode=atomic ${d}
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -f "./codecov.sh" ]; then
|
||||
./codecov.sh
|
||||
fi
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data"
|
||||
XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata"
|
||||
)
|
||||
|
||||
type clientAvatarData struct {
|
||||
XMLName xml.Name `xml:"data"`
|
||||
Data []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type clientAvatarInfo struct {
|
||||
XMLName xml.Name `xml:"info"`
|
||||
Bytes string `xml:"bytes,attr"`
|
||||
Width string `xml:"width,attr"`
|
||||
Height string `xml:"height,attr"`
|
||||
ID string `xml:"id,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
URL string `xml:"url,attr"`
|
||||
}
|
||||
|
||||
type clientAvatarMetadata struct {
|
||||
XMLName xml.Name `xml:"metadata"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
Info clientAvatarInfo `xml:"info"`
|
||||
}
|
||||
|
||||
type AvatarData struct {
|
||||
Data []byte
|
||||
From string
|
||||
}
|
||||
|
||||
type AvatarMetadata struct {
|
||||
From string
|
||||
Bytes int
|
||||
Width int
|
||||
Height int
|
||||
ID string
|
||||
Type string
|
||||
URL string
|
||||
}
|
||||
|
||||
func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
|
||||
var data clientAvatarData
|
||||
err := xml.Unmarshal(itemsBody, &data)
|
||||
if err != nil {
|
||||
return AvatarData{}, err
|
||||
}
|
||||
|
||||
// Base64-decode the avatar data to check its SHA1 hash
|
||||
dataRaw, err := base64.StdEncoding.DecodeString(
|
||||
string(data.Data))
|
||||
if err != nil {
|
||||
return AvatarData{}, err
|
||||
}
|
||||
|
||||
hash := sha1.Sum(dataRaw)
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
if hashStr != id {
|
||||
return AvatarData{}, errors.New("SHA1 hashes do not match")
|
||||
}
|
||||
|
||||
return AvatarData{
|
||||
Data: dataRaw,
|
||||
From: from,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) {
|
||||
var meta clientAvatarMetadata
|
||||
err := xml.Unmarshal(body, &meta)
|
||||
if err != nil {
|
||||
return AvatarMetadata{}, err
|
||||
}
|
||||
|
||||
return AvatarMetadata{
|
||||
From: from,
|
||||
Bytes: atoiw(meta.Info.Bytes),
|
||||
Width: atoiw(meta.Info.Width),
|
||||
Height: atoiw(meta.Info.Height),
|
||||
ID: meta.Info.ID,
|
||||
Type: meta.Info.Type,
|
||||
URL: meta.Info.URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// A wrapper for atoi which just returns -1 if an error occurs
|
||||
func atoiw(str string) int {
|
||||
i, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *Client) AvatarSubscribeMetadata(jid string) {
|
||||
c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||
}
|
||||
|
||||
func (c *Client) AvatarUnsubscribeMetadata(jid string) {
|
||||
c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||
}
|
||||
|
||||
func (c *Client) AvatarRequestData(jid string) {
|
||||
c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid)
|
||||
}
|
||||
|
||||
func (c *Client) AvatarRequestDataByID(jid, id string) {
|
||||
c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id)
|
||||
}
|
||||
|
||||
func (c *Client) AvatarRequestMetadata(jid string) {
|
||||
c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
const (
|
||||
XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
|
||||
XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info"
|
||||
)
|
||||
|
||||
type clientDiscoFeature struct {
|
||||
XMLName xml.Name `xml:"feature"`
|
||||
Var string `xml:"var,attr"`
|
||||
}
|
||||
|
||||
type clientDiscoIdentity struct {
|
||||
XMLName xml.Name `xml:"identity"`
|
||||
Category string `xml:"category,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
}
|
||||
|
||||
type clientDiscoQuery struct {
|
||||
XMLName xml.Name `xml:"query"`
|
||||
Features []clientDiscoFeature `xml:"feature"`
|
||||
Identities []clientDiscoIdentity `xml:"identity"`
|
||||
}
|
||||
|
||||
type clientDiscoItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Jid string `xml:"jid,attr"`
|
||||
Node string `xml:"node,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
}
|
||||
|
||||
type clientDiscoItemsQuery struct {
|
||||
XMLName xml.Name `xml:"query"`
|
||||
Items []clientDiscoItem `xml:"item"`
|
||||
}
|
||||
|
||||
type DiscoIdentity struct {
|
||||
Category string
|
||||
Type string
|
||||
Name string
|
||||
}
|
||||
|
||||
type DiscoItem struct {
|
||||
Jid string
|
||||
Name string
|
||||
Node string
|
||||
}
|
||||
|
||||
type DiscoResult struct {
|
||||
Features []string
|
||||
Identities []DiscoIdentity
|
||||
}
|
||||
|
||||
type DiscoItems struct {
|
||||
Jid string
|
||||
Items []DiscoItem
|
||||
}
|
||||
|
||||
func clientFeaturesToReturn(features []clientDiscoFeature) []string {
|
||||
var ret []string
|
||||
|
||||
for _, feature := range features {
|
||||
ret = append(ret, feature.Var)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func clientIdentitiesToReturn(identities []clientDiscoIdentity) []DiscoIdentity {
|
||||
var ret []DiscoIdentity
|
||||
|
||||
for _, id := range identities {
|
||||
ret = append(ret, DiscoIdentity{
|
||||
Category: id.Category,
|
||||
Type: id.Type,
|
||||
Name: id.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem {
|
||||
var ret []DiscoItem
|
||||
for _, item := range items {
|
||||
ret = append(ret, DiscoItem{
|
||||
Jid: item.Jid,
|
||||
Name: item.Name,
|
||||
Node: item.Node,
|
||||
})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
IQTypeGet = "get"
|
||||
IQTypeSet = "set"
|
||||
IQTypeResult = "result"
|
||||
)
|
||||
|
||||
func (c *Client) Discovery() (string, error) {
|
||||
// use UUIDv4 for a pseudo random id.
|
||||
reqID := strconv.FormatUint(uint64(getCookie()), 10)
|
||||
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "")
|
||||
}
|
||||
|
||||
// Discover information about a node
|
||||
func (c *Client) DiscoverNodeInfo(node string) (string, error) {
|
||||
query := fmt.Sprintf("<query xmlns='%s' node='%s'/>", XMPPNS_DISCO_INFO, node)
|
||||
return c.RawInformation(c.jid, c.domain, "info3", IQTypeGet, query)
|
||||
}
|
||||
|
||||
// Discover items that the server exposes
|
||||
func (c *Client) DiscoverServerItems() (string, error) {
|
||||
return c.DiscoverEntityItems(c.domain)
|
||||
}
|
||||
|
||||
// Discover items that an entity exposes
|
||||
func (c *Client) DiscoverEntityItems(jid string) (string, error) {
|
||||
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_ITEMS)
|
||||
return c.RawInformation(c.jid, jid, "info1", IQTypeGet, query)
|
||||
}
|
||||
|
||||
// RawInformationQuery sends an information query request to the server.
|
||||
func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) {
|
||||
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'><query xmlns='%s'>%s</query></iq>\n"
|
||||
_, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// rawInformation send a IQ request with the payload body to the server
|
||||
func (c *Client) RawInformation(from, to, id, iqType, body string) (string, error) {
|
||||
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'>%s</iq>\n"
|
||||
_, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body)
|
||||
return id, err
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
// Copyright 2013 Flo Lauber <dev@qatfy.at>. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO(flo):
|
||||
// - support password protected MUC rooms
|
||||
// - cleanup signatures of join/leave functions
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
nsMUC = "http://jabber.org/protocol/muc"
|
||||
nsMUCUser = "http://jabber.org/protocol/muc#user"
|
||||
NoHistory = 0
|
||||
CharHistory = 1
|
||||
StanzaHistory = 2
|
||||
SecondsHistory = 3
|
||||
SinceHistory = 4
|
||||
)
|
||||
|
||||
// Send sends room topic wrapped inside an XMPP message stanza body.
|
||||
func (c *Client) SendTopic(chat Chat) (n int, err error) {
|
||||
return fmt.Fprintf(c.stanzaWriter, "<message to='%s' type='%s' xml:lang='en'>"+"<subject>%s</subject></message>\n",
|
||||
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
|
||||
}
|
||||
|
||||
func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
|
||||
if nick == "" {
|
||||
nick = c.jid
|
||||
}
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<history maxchars='0'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
||||
}
|
||||
|
||||
// xep-0045 7.2
|
||||
func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) {
|
||||
if nick == "" {
|
||||
nick = c.jid
|
||||
}
|
||||
switch history_type {
|
||||
case NoHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s' />"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
||||
case CharHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<history maxchars='%d'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
||||
case StanzaHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<history maxstanzas='%d'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
||||
case SecondsHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<history seconds='%d'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
||||
case SinceHistory:
|
||||
if history_date != nil {
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<history since='%s'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
return 0, errors.New("Unknown history option")
|
||||
}
|
||||
|
||||
// xep-0045 7.2.6
|
||||
func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) {
|
||||
if nick == "" {
|
||||
nick = c.jid
|
||||
}
|
||||
switch history_type {
|
||||
case NoHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<password>%s</password>"+
|
||||
"</x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
|
||||
case CharHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<password>%s</password>"+
|
||||
"<history maxchars='%d'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
||||
case StanzaHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<password>%s</password>"+
|
||||
"<history maxstanzas='%d'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
||||
case SecondsHistory:
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<password>%s</password>"+
|
||||
"<history seconds='%d'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
||||
case SinceHistory:
|
||||
if history_date != nil {
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||
"<x xmlns='%s'>"+
|
||||
"<password>%s</password>"+
|
||||
"<history since='%s'/></x>"+
|
||||
"</presence>\n",
|
||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
return 0, errors.New("Unknown history option")
|
||||
}
|
||||
|
||||
// xep-0045 7.14
|
||||
func (c *Client) LeaveMUC(jid string) (n int, err error) {
|
||||
return fmt.Fprintf(c.stanzaWriter, "<presence from='%s' to='%s' type='unavailable' />\n",
|
||||
c.jid, xmlEscape(jid))
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (c *Client) PingC2S(jid, server string) error {
|
||||
if jid == "" {
|
||||
jid = c.jid
|
||||
}
|
||||
if server == "" {
|
||||
server = c.domain
|
||||
}
|
||||
_, err := fmt.Fprintf(c.stanzaWriter, "<iq from='%s' to='%s' id='c2s1' type='get'>"+
|
||||
"<ping xmlns='urn:xmpp:ping'/>"+
|
||||
"</iq>\n",
|
||||
xmlEscape(jid), xmlEscape(server))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) PingS2S(fromServer, toServer string) error {
|
||||
_, err := fmt.Fprintf(c.stanzaWriter, "<iq from='%s' to='%s' id='s2s1' type='get'>"+
|
||||
"<ping xmlns='urn:xmpp:ping'/>"+
|
||||
"</iq>\n",
|
||||
xmlEscape(fromServer), xmlEscape(toServer))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) SendResultPing(id, toServer string) error {
|
||||
_, err := fmt.Fprintf(c.stanzaWriter, "<iq type='result' to='%s' id='%s'/>\n",
|
||||
xmlEscape(toServer), xmlEscape(id))
|
||||
return err
|
||||
}
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub"
|
||||
XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event"
|
||||
)
|
||||
|
||||
type clientPubsubItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
ID string `xml:"id,attr"`
|
||||
Body []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type clientPubsubItems struct {
|
||||
XMLName xml.Name `xml:"items"`
|
||||
Node string `xml:"node,attr"`
|
||||
Items []clientPubsubItem `xml:"item"`
|
||||
}
|
||||
|
||||
type clientPubsubEvent struct {
|
||||
XMLName xml.Name `xml:"event"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
Items clientPubsubItems `xml:"items"`
|
||||
}
|
||||
|
||||
type clientPubsubError struct {
|
||||
XMLName xml.Name
|
||||
}
|
||||
|
||||
type clientPubsubSubscription struct {
|
||||
XMLName xml.Name `xml:"subscription"`
|
||||
Node string `xml:"node,attr"`
|
||||
JID string `xml:"jid,attr"`
|
||||
SubID string `xml:"subid,attr"`
|
||||
}
|
||||
|
||||
type PubsubEvent struct {
|
||||
Node string
|
||||
Items []PubsubItem
|
||||
}
|
||||
|
||||
type PubsubSubscription struct {
|
||||
SubID string
|
||||
JID string
|
||||
Node string
|
||||
Errors []string
|
||||
}
|
||||
type PubsubUnsubscription PubsubSubscription
|
||||
|
||||
type PubsubItem struct {
|
||||
ID string
|
||||
InnerXML []byte
|
||||
}
|
||||
|
||||
type PubsubItems struct {
|
||||
Node string
|
||||
Items []PubsubItem
|
||||
}
|
||||
|
||||
// Converts []clientPubsubItem to []PubsubItem
|
||||
func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem {
|
||||
var tmp []PubsubItem
|
||||
for _, i := range items {
|
||||
tmp = append(tmp, PubsubItem{
|
||||
ID: i.ID,
|
||||
InnerXML: i.Body,
|
||||
})
|
||||
}
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
func pubsubClientToReturn(event clientPubsubEvent) PubsubEvent {
|
||||
return PubsubEvent{
|
||||
Node: event.Items.Node,
|
||||
Items: pubsubItemsToReturn(event.Items.Items),
|
||||
}
|
||||
}
|
||||
|
||||
func pubsubStanza(body string) string {
|
||||
return fmt.Sprintf("<pubsub xmlns='%s'>%s</pubsub>",
|
||||
XMPPNS_PUBSUB, body)
|
||||
}
|
||||
|
||||
func pubsubSubscriptionStanza(node, jid string) string {
|
||||
body := fmt.Sprintf("<subscribe node='%s' jid='%s'/>",
|
||||
xmlEscape(node),
|
||||
xmlEscape(jid))
|
||||
return pubsubStanza(body)
|
||||
}
|
||||
|
||||
func pubsubUnsubscriptionStanza(node, jid string) string {
|
||||
body := fmt.Sprintf("<unsubscribe node='%s' jid='%s'/>",
|
||||
xmlEscape(node),
|
||||
xmlEscape(jid))
|
||||
return pubsubStanza(body)
|
||||
}
|
||||
|
||||
func (c *Client) PubsubSubscribeNode(node, jid string) {
|
||||
c.RawInformation(c.jid,
|
||||
jid,
|
||||
"sub1",
|
||||
"set",
|
||||
pubsubSubscriptionStanza(node, c.jid))
|
||||
}
|
||||
|
||||
func (c *Client) PubsubUnsubscribeNode(node, jid string) {
|
||||
c.RawInformation(c.jid,
|
||||
jid,
|
||||
"unsub1",
|
||||
"set",
|
||||
pubsubUnsubscriptionStanza(node, c.jid))
|
||||
}
|
||||
|
||||
func (c *Client) PubsubRequestLastItems(node, jid string) {
|
||||
body := fmt.Sprintf("<items node='%s'/>", node)
|
||||
c.RawInformation(c.jid, jid, "items1", "get", pubsubStanza(body))
|
||||
}
|
||||
|
||||
func (c *Client) PubsubRequestItem(node, jid, id string) {
|
||||
body := fmt.Sprintf("<items node='%s'><item id='%s'/></items>", node, id)
|
||||
c.RawInformation(c.jid, jid, "items3", "get", pubsubStanza(body))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user