forked from jshiffer/go-xmpp
Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b04299b40c | |||
| 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
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are met:
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
notice, this list of conditions and the following disclaimer.
|
list of conditions and the following disclaimer.
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
in the documentation and/or other materials provided with the
|
this list of conditions and the following disclaimer in the documentation
|
||||||
distribution.
|
and/or other materials provided with the distribution.
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|||||||
@@ -1,6 +1,133 @@
|
|||||||
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/mattn/go-xmpp)
|
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
||||||
|
|
||||||
|
The goal is to make simple to write simple XMPP clients and components:
|
||||||
|
|
||||||
|
- For automation (like for example monitoring of an XMPP service),
|
||||||
|
- For building connected "things" by plugging them on an XMPP server,
|
||||||
|
- For writing simple chatbot to control a service or a thing.
|
||||||
|
- For writing XMPP servers components.
|
||||||
|
|
||||||
|
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
|
||||||
|
|
||||||
|
## Configuration and connection
|
||||||
|
|
||||||
|
### Allowing Insecure TLS connection during development
|
||||||
|
|
||||||
|
It is not recommended to disable the check for domain name and certificate chain. Doing so would open your client
|
||||||
|
to man-in-the-middle attacks.
|
||||||
|
|
||||||
|
However, in development, XMPP servers often use self-signed certificates. In that situation, it is better to add the
|
||||||
|
root CA that signed the certificate to your trusted list of root CA. It avoids changing the code and limit the risk
|
||||||
|
of shipping an insecure client to production.
|
||||||
|
|
||||||
|
That said, if you really want to allow your client to trust any TLS certificate, you can customize Go standard
|
||||||
|
`tls.Config` and set it in Config struct.
|
||||||
|
|
||||||
|
Here is an example code to configure a client to allow connecting to a server with self-signed certificate. Note the
|
||||||
|
`InsecureSkipVerify` option. When using this `tls.Config` option, all the checks on the certificate are skipped.
|
||||||
|
|
||||||
|
```go
|
||||||
|
config := xmpp.Config{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Password: "test",
|
||||||
|
TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported specifications
|
||||||
|
|
||||||
|
### Clients
|
||||||
|
|
||||||
|
- [RFC 6120: XMPP Core](https://xmpp.org/rfcs/rfc6120.html)
|
||||||
|
- [RFC 6121: XMPP Instant Messaging and Presence](https://xmpp.org/rfcs/rfc6121.html)
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- [XEP-0114: Jabber Component Protocol](https://xmpp.org/extensions/xep-0114.html)
|
||||||
|
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
|
||||||
|
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
|
||||||
|
|
||||||
|
## Stanza subpackage
|
||||||
|
|
||||||
|
XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to
|
||||||
|
leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas
|
||||||
|
back and forth.
|
||||||
|
|
||||||
|
At a low-level, stanzas are XML fragments. However, Fluux XMPP library provides the building blocks to interact with
|
||||||
|
stanzas at a high-level, providing a Go-friendly API.
|
||||||
|
|
||||||
|
The `stanza` subpackage provides support for XMPP stream parsing, marshalling and unmarshalling of XMPP stanza. It is a
|
||||||
|
bridge between high-level Go structure and low-level XMPP protocol.
|
||||||
|
|
||||||
|
Parsing, marshalling and unmarshalling is automatically handled by Fluux XMPP client library. As a developer, you will
|
||||||
|
generally manipulates only the high-level structs provided by the stanza package.
|
||||||
|
|
||||||
|
The XMPP protocol, as the name implies is extensible. If your application is using custom stanza extensions, you can
|
||||||
|
implement your own extensions directly in your own application.
|
||||||
|
|
||||||
|
To learn more about the stanza package, you can read more in the
|
||||||
|
[stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using
|
||||||
|
Fluux XMPP library.
|
||||||
|
|
||||||
|
Here is the demo "echo" client:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Password: "test",
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(config, router)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
|
_ = s.Send(reply)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference documentation
|
||||||
|
|
||||||
|
The code documentation is available on GoDoc: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"github.com/mattn/go-gtk/gtk"
|
|
||||||
"github.com/mattn/go-xmpp"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
gtk.Init(&os.Args)
|
|
||||||
|
|
||||||
window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL)
|
|
||||||
window.SetTitle("GoTalk")
|
|
||||||
window.Connect("destroy", func() {
|
|
||||||
gtk.MainQuit()
|
|
||||||
})
|
|
||||||
vbox := gtk.NewVBox(false, 1)
|
|
||||||
scrolledwin := gtk.NewScrolledWindow(nil, nil)
|
|
||||||
textview := gtk.NewTextView()
|
|
||||||
textview.SetEditable(false)
|
|
||||||
textview.SetCursorVisible(false)
|
|
||||||
scrolledwin.Add(textview)
|
|
||||||
vbox.Add(scrolledwin)
|
|
||||||
|
|
||||||
buffer := textview.GetBuffer()
|
|
||||||
|
|
||||||
entry := gtk.NewEntry()
|
|
||||||
vbox.PackEnd(entry, false, false, 0)
|
|
||||||
|
|
||||||
window.Add(vbox)
|
|
||||||
window.SetSizeRequest(300, 400)
|
|
||||||
window.ShowAll()
|
|
||||||
|
|
||||||
dialog := gtk.NewDialog()
|
|
||||||
dialog.SetTitle(window.GetTitle())
|
|
||||||
sgroup := gtk.NewSizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
|
|
||||||
|
|
||||||
hbox := gtk.NewHBox(false, 1)
|
|
||||||
dialog.GetVBox().Add(hbox)
|
|
||||||
label := gtk.NewLabel("username:")
|
|
||||||
sgroup.AddWidget(label)
|
|
||||||
hbox.Add(label)
|
|
||||||
username := gtk.NewEntry()
|
|
||||||
hbox.Add(username)
|
|
||||||
|
|
||||||
hbox = gtk.NewHBox(false, 1)
|
|
||||||
dialog.GetVBox().Add(hbox)
|
|
||||||
label = gtk.NewLabel("password:")
|
|
||||||
sgroup.AddWidget(label)
|
|
||||||
hbox.Add(label)
|
|
||||||
password := gtk.NewEntry()
|
|
||||||
password.SetVisibility(false)
|
|
||||||
hbox.Add(password)
|
|
||||||
|
|
||||||
dialog.AddButton(gtk.STOCK_OK, gtk.RESPONSE_OK)
|
|
||||||
dialog.AddButton(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
|
||||||
dialog.SetDefaultResponse(gtk.RESPONSE_OK)
|
|
||||||
dialog.SetTransientFor(window)
|
|
||||||
dialog.ShowAll()
|
|
||||||
res := dialog.Run()
|
|
||||||
username_ := username.GetText()
|
|
||||||
password_ := password.GetText()
|
|
||||||
dialog.Destroy()
|
|
||||||
if res != gtk.RESPONSE_OK {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
xmpp.DefaultConfig = tls.Config{
|
|
||||||
ServerName: "talk.google.com",
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
talk, err := xmpp.NewClient("talk.google.com:443", username_, password_, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.Connect("activate", func() {
|
|
||||||
text := entry.GetText()
|
|
||||||
tokens := strings.SplitN(text, " ", 2)
|
|
||||||
if len(tokens) == 2 {
|
|
||||||
func() {
|
|
||||||
defer recover()
|
|
||||||
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
|
|
||||||
entry.SetText("")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
func() {
|
|
||||||
defer recover()
|
|
||||||
chat, err := talk.Recv()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var iter gtk.TextIter
|
|
||||||
buffer.GetStartIter(&iter)
|
|
||||||
if msg, ok := chat.(xmpp.Chat); ok {
|
|
||||||
buffer.Insert(&iter, msg.Remote+": "+msg.Text+"\n")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
gtk.Main()
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"crypto/tls"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/mattn/go-xmpp"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var server = flag.String("server", "talk.google.com:443", "server")
|
|
||||||
var username = flag.String("username", "", "username")
|
|
||||||
var password = flag.String("password", "", "password")
|
|
||||||
var status = flag.String("status", "xa", "status")
|
|
||||||
var statusMessage = flag.String("status-msg", "I for one welcome our new codebot overlords.", "status message")
|
|
||||||
var notls = flag.Bool("notls", false, "No TLS")
|
|
||||||
var debug = flag.Bool("debug", false, "debug output")
|
|
||||||
var session = flag.Bool("session", false, "use server session")
|
|
||||||
|
|
||||||
func serverName(host string) string {
|
|
||||||
return strings.Split(host, ":")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Usage = func() {
|
|
||||||
fmt.Fprintf(os.Stderr, "usage: example [options]\n")
|
|
||||||
flag.PrintDefaults()
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
flag.Parse()
|
|
||||||
if *username == "" || *password == "" {
|
|
||||||
if *debug && *username == "" && *password == "" {
|
|
||||||
fmt.Fprintf(os.Stderr, "no username or password were given; attempting ANONYMOUS auth\n")
|
|
||||||
} else if *username != "" || *password != "" {
|
|
||||||
flag.Usage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*notls {
|
|
||||||
xmpp.DefaultConfig = tls.Config{
|
|
||||||
ServerName: serverName(*server),
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var talk *xmpp.Client
|
|
||||||
var err error
|
|
||||||
options := xmpp.Options{Host: *server,
|
|
||||||
User: *username,
|
|
||||||
Password: *password,
|
|
||||||
NoTLS: *notls,
|
|
||||||
Debug: *debug,
|
|
||||||
Session: *session,
|
|
||||||
Status: *status,
|
|
||||||
StatusMessage: *statusMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
talk, err = options.NewClient()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
chat, err := talk.Recv()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
switch v := chat.(type) {
|
|
||||||
case xmpp.Chat:
|
|
||||||
fmt.Println(v.Remote, v.Text)
|
|
||||||
case xmpp.Presence:
|
|
||||||
fmt.Println(v.From, v.Show)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
in := bufio.NewReader(os.Stdin)
|
|
||||||
line, err := in.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
line = strings.TrimRight(line, "\n")
|
|
||||||
|
|
||||||
tokens := strings.SplitN(line, " ", 2)
|
|
||||||
if len(tokens) == 2 {
|
|
||||||
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,53 @@
|
|||||||
|
/*
|
||||||
|
xmpp_echo is a demo client that connect on an XMPP server and echo message received back to original sender.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Password: "test",
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(config, router)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
|
_ = s.Send(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
|
||||||
|
// (using templates ?)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Should I move this as an extension of the client?
|
||||||
|
// I should probably make the code more modular, but keep concern separated to keep it simple.
|
||||||
|
type ServerCheck struct {
|
||||||
|
address string
|
||||||
|
domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChecker(address, domain string) (*ServerCheck, error) {
|
||||||
|
client := ServerCheck{}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var host string
|
||||||
|
if client.address, host, err = extractParams(address); err != nil {
|
||||||
|
return &client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if domain != "" {
|
||||||
|
client.domain = domain
|
||||||
|
} else {
|
||||||
|
client.domain = host
|
||||||
|
}
|
||||||
|
|
||||||
|
return &client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check triggers actual TCP connection, based on previously defined parameters.
|
||||||
|
func (c *ServerCheck) Check() error {
|
||||||
|
var tcpconn net.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
timeout := 15 * time.Second
|
||||||
|
tcpconn, err = net.DialTimeout("tcp", c.address, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := xml.NewDecoder(tcpconn)
|
||||||
|
|
||||||
|
// Send stream open tag
|
||||||
|
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set xml decoder and extract streamID from reply (not used for now)
|
||||||
|
_, err = stanza.InitStream(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract stream features
|
||||||
|
var f stanza.StreamFeatures
|
||||||
|
packet, err := stanza.NextPacket(decoder)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("stream open decode features: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p := packet.(type) {
|
||||||
|
case stanza.StreamFeatures:
|
||||||
|
f = p
|
||||||
|
case stanza.StreamError:
|
||||||
|
return errors.New("open stream error: " + p.Error.Local)
|
||||||
|
default:
|
||||||
|
return errors.New("expected packet received while expecting features, got " + p.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := f.DoesStartTLS(); ok {
|
||||||
|
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
|
||||||
|
var k stanza.TLSProceed
|
||||||
|
if err = decoder.DecodeElement(&k, nil); err != nil {
|
||||||
|
return fmt.Errorf("expecting starttls proceed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tlsConfig tls.Config
|
||||||
|
tlsConfig.ServerName = c.domain
|
||||||
|
tlsConn := tls.Client(tcpconn, &tlsConfig)
|
||||||
|
// We convert existing connection to TLS
|
||||||
|
if err = tlsConn.Handshake(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We check that cert matches hostname
|
||||||
|
if err = tlsConn.VerifyHostname(c.domain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = checkExpiration(tlsConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("TLS not supported on server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expiration date for the whole certificate chain and returns an error
|
||||||
|
// if the expiration date is in less than 48 hours.
|
||||||
|
func checkExpiration(tlsConn *tls.Conn) error {
|
||||||
|
checkedCerts := make(map[string]struct{})
|
||||||
|
for _, chain := range tlsConn.ConnectionState().VerifiedChains {
|
||||||
|
for _, cert := range chain {
|
||||||
|
if _, checked := checkedCerts[string(cert.Signature)]; checked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
checkedCerts[string(cert.Signature)] = struct{}{}
|
||||||
|
|
||||||
|
// Check the expiration.
|
||||||
|
timeNow := time.Now()
|
||||||
|
expiresInHours := int64(cert.NotAfter.Sub(timeNow).Hours())
|
||||||
|
// fmt.Printf("Cert '%s' expires in %d days\n", cert.Subject.CommonName, expiresInHours/24)
|
||||||
|
if expiresInHours <= 48 {
|
||||||
|
return fmt.Errorf("certificate '%s' will expire on %s", cert.Subject.CommonName, cert.NotAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractParams(addr string) (string, string, error) {
|
||||||
|
var err error
|
||||||
|
hostport := strings.Split(addr, ":")
|
||||||
|
if len(hostport) > 2 {
|
||||||
|
err = errors.New("too many colons in xmpp server address")
|
||||||
|
return addr, hostport[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address is composed of two parts, we are good
|
||||||
|
if len(hostport) == 2 && hostport[1] != "" {
|
||||||
|
return addr, hostport[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port was not passed, we append XMPP default port:
|
||||||
|
return strings.Join([]string{hostport[0], "5222"}, ":"), hostport[0], err
|
||||||
|
}
|
||||||
@@ -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,22 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Address string
|
||||||
|
Jid string
|
||||||
|
parsedJid *Jid // For easier manipulation
|
||||||
|
Password string
|
||||||
|
StreamLogger *os.File // Used for debugging
|
||||||
|
Lang string // TODO: should default to 'en'
|
||||||
|
ConnectTimeout int // Client timeout in seconds. Default to 15
|
||||||
|
TLSConfig tls.Config
|
||||||
|
// Insecure can be set to true to allow to open a session without TLS. If TLS
|
||||||
|
// is supported on the server, we will still try to use it.
|
||||||
|
Insecure bool
|
||||||
|
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
module gosrc.io/xmpp
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/go-cmp v0.2.0
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
// Session info
|
||||||
|
BindJid string // Jabber ID as provided by XMPP server
|
||||||
|
StreamId string
|
||||||
|
Features stanza.StreamFeatures
|
||||||
|
TlsEnabled bool
|
||||||
|
lastPacketId int
|
||||||
|
|
||||||
|
// read / write
|
||||||
|
streamLogger io.ReadWriter
|
||||||
|
decoder *xml.Decoder
|
||||||
|
|
||||||
|
// error management
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
|
||||||
|
s := new(Session)
|
||||||
|
s.init(conn, o)
|
||||||
|
|
||||||
|
// starttls
|
||||||
|
var tlsConn net.Conn
|
||||||
|
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o)
|
||||||
|
|
||||||
|
if !s.TlsEnabled && !o.Insecure {
|
||||||
|
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
||||||
|
return nil, nil, NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TlsEnabled {
|
||||||
|
s.reset(conn, tlsConn, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth
|
||||||
|
s.auth(o)
|
||||||
|
s.reset(tlsConn, tlsConn, o)
|
||||||
|
|
||||||
|
// bind resource and 'start' XMPP session
|
||||||
|
s.bind(o)
|
||||||
|
s.rfc3921Session(o)
|
||||||
|
|
||||||
|
return tlsConn, s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) PacketId() string {
|
||||||
|
s.lastPacketId++
|
||||||
|
return fmt.Sprintf("%x", s.lastPacketId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) init(conn net.Conn, o Config) {
|
||||||
|
s.setStreamLogger(nil, conn, o)
|
||||||
|
s.Features = s.open(o.parsedJid.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) reset(conn net.Conn, newConn net.Conn, o Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.setStreamLogger(conn, newConn, o)
|
||||||
|
s.Features = s.open(o.parsedJid.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) setStreamLogger(conn net.Conn, newConn net.Conn, o Config) {
|
||||||
|
if newConn != conn {
|
||||||
|
s.streamLogger = newStreamLogger(newConn, o.StreamLogger)
|
||||||
|
}
|
||||||
|
s.decoder = xml.NewDecoder(s.streamLogger)
|
||||||
|
s.decoder.CharsetReader = o.CharsetReader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
||||||
|
// Send stream open tag
|
||||||
|
if _, s.err = fmt.Fprintf(s.streamLogger, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set xml decoder and extract streamID from reply
|
||||||
|
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract stream features
|
||||||
|
if s.err = s.decoder.Decode(&f); s.err != nil {
|
||||||
|
s.err = errors.New("stream open decode features: " + s.err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) net.Conn {
|
||||||
|
if s.err != nil {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := s.Features.DoesStartTLS(); ok {
|
||||||
|
fmt.Fprintf(s.streamLogger, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
|
||||||
|
var k stanza.TLSProceed
|
||||||
|
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
|
||||||
|
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
o.TLSConfig.ServerName = domain
|
||||||
|
tlsConn := tls.Client(conn, &o.TLSConfig)
|
||||||
|
// We convert existing connection to TLS
|
||||||
|
if s.err = tlsConn.Handshake(); s.err != nil {
|
||||||
|
return tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.TLSConfig.InsecureSkipVerify {
|
||||||
|
// We check that cert matches hostname
|
||||||
|
s.err = tlsConn.VerifyHostname(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.err == nil {
|
||||||
|
s.TlsEnabled = true
|
||||||
|
}
|
||||||
|
return tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we do not allow cleartext connections, make it explicit that server do not support starttls
|
||||||
|
if !o.Insecure {
|
||||||
|
s.err = errors.New("XMPP server does not advertise support for starttls")
|
||||||
|
}
|
||||||
|
|
||||||
|
// starttls is not supported => we do not upgrade the connection:
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) auth(o Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) bind(o Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send IQ message asking to bind to the local user name.
|
||||||
|
var resource = o.parsedJid.Resource
|
||||||
|
if resource != "" {
|
||||||
|
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
||||||
|
s.PacketId(), stanza.NSBind, resource)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
var iq stanza.IQ
|
||||||
|
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
||||||
|
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Check all elements
|
||||||
|
switch payload := iq.Payload.(type) {
|
||||||
|
case *stanza.Bind:
|
||||||
|
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
|
||||||
|
default:
|
||||||
|
s.err = errors.New("iq bind result missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the bind, if the session is not optional (as per old RFC 3921), we send the session open iq.
|
||||||
|
func (s *Session) rfc3921Session(o Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var iq stanza.IQ
|
||||||
|
// We only negotiate session binding if it is mandatory, we skip it when optional.
|
||||||
|
if !s.Features.Session.IsOptional() {
|
||||||
|
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
|
||||||
|
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
||||||
|
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,14 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Used during stream initiation / session establishment
|
||||||
|
type TLSProceed struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tlsFailure struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
|
||||||
|
}
|
||||||
@@ -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 %#v", 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
|
||||||
@@ -1,958 +0,0 @@
|
|||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// TODO(rsc):
|
|
||||||
// More precise error handling.
|
|
||||||
// Presence functionality.
|
|
||||||
// TODO(mattn):
|
|
||||||
// Add proxy authentication.
|
|
||||||
|
|
||||||
// Package xmpp implements a simple Google Talk client
|
|
||||||
// using the XMPP protocol described in RFC 3920 and RFC 3921.
|
|
||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
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"
|
|
||||||
nsClient = "jabber:client"
|
|
||||||
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Default TLS configuration options
|
|
||||||
var DefaultConfig tls.Config
|
|
||||||
|
|
||||||
// Cookie is a unique XMPP session identifier
|
|
||||||
type Cookie uint64
|
|
||||||
|
|
||||||
func getCookie() Cookie {
|
|
||||||
var buf [8]byte
|
|
||||||
if _, err := rand.Reader.Read(buf[:]); err != nil {
|
|
||||||
panic("Failed to read random bytes: " + err.Error())
|
|
||||||
}
|
|
||||||
return Cookie(binary.LittleEndian.Uint64(buf[:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client holds XMPP connection opitons
|
|
||||||
type Client struct {
|
|
||||||
conn net.Conn // connection to server
|
|
||||||
jid string // Jabber ID for our connection
|
|
||||||
domain string
|
|
||||||
p *xml.Decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) JID() string {
|
|
||||||
return c.jid
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsIgnoreCase(s, substr string) bool {
|
|
||||||
s, substr = strings.ToUpper(s), strings.ToUpper(substr)
|
|
||||||
return strings.Contains(s, substr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func connect(host, user, passwd string) (net.Conn, error) {
|
|
||||||
addr := host
|
|
||||||
|
|
||||||
if strings.TrimSpace(host) == "" {
|
|
||||||
a := strings.SplitN(user, "@", 2)
|
|
||||||
if len(a) == 2 {
|
|
||||||
addr = a[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a := strings.SplitN(host, ":", 2)
|
|
||||||
if len(a) == 1 {
|
|
||||||
addr += ":5222"
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := os.Getenv("HTTP_PROXY")
|
|
||||||
if proxy == "" {
|
|
||||||
proxy = os.Getenv("http_proxy")
|
|
||||||
}
|
|
||||||
// test for no proxy, takes a comma separated list with substrings to match
|
|
||||||
if proxy != "" {
|
|
||||||
noproxy := os.Getenv("NO_PROXY")
|
|
||||||
if noproxy == "" {
|
|
||||||
noproxy = os.Getenv("no_proxy")
|
|
||||||
}
|
|
||||||
if noproxy != "" {
|
|
||||||
nplist := strings.Split(noproxy, ",")
|
|
||||||
for _, s := range nplist {
|
|
||||||
if containsIgnoreCase(addr, s) {
|
|
||||||
proxy = ""
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if proxy != "" {
|
|
||||||
url, err := url.Parse(proxy)
|
|
||||||
if err == nil {
|
|
||||||
addr = url.Host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := net.Dial("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if proxy != "" {
|
|
||||||
fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host)
|
|
||||||
fmt.Fprintf(c, "Host: %s\r\n", host)
|
|
||||||
fmt.Fprintf(c, "\r\n")
|
|
||||||
br := bufio.NewReader(c)
|
|
||||||
req, _ := http.NewRequest("CONNECT", host, nil)
|
|
||||||
resp, err := http.ReadResponse(br, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
f := strings.SplitN(resp.Status, " ", 2)
|
|
||||||
return nil, errors.New(f[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options are used to specify additional options for new clients, such as a Resource.
|
|
||||||
type Options struct {
|
|
||||||
// Host specifies what host to connect to, as either "hostname" or "hostname:port"
|
|
||||||
// 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.
|
|
||||||
Host string
|
|
||||||
|
|
||||||
// User specifies what user to authenticate to the remote server.
|
|
||||||
User string
|
|
||||||
|
|
||||||
// Password supplies the password to use for authentication with the remote server.
|
|
||||||
Password string
|
|
||||||
|
|
||||||
// Resource specifies an XMPP client resource, like "bot", instead of accepting one
|
|
||||||
// from the server. Use "" to let the server generate one for your client.
|
|
||||||
Resource string
|
|
||||||
|
|
||||||
// OAuthScope provides go-xmpp the required scope for OAuth2 authentication.
|
|
||||||
OAuthScope string
|
|
||||||
|
|
||||||
// OAuthToken provides go-xmpp with the required OAuth2 token used to authenticate
|
|
||||||
OAuthToken string
|
|
||||||
|
|
||||||
// OAuthXmlNs provides go-xmpp with the required namespaced used for OAuth2 authentication. This is
|
|
||||||
// provided to the server as the xmlns:auth attribute of the OAuth2 authentication request.
|
|
||||||
OAuthXmlNs string
|
|
||||||
|
|
||||||
// TLS Config
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
|
|
||||||
// InsecureAllowUnencryptedAuth permits authentication over a TCP connection that has not been promoted to
|
|
||||||
// TLS by STARTTLS; this could leak authentication information over the network, or permit man in the middle
|
|
||||||
// attacks.
|
|
||||||
InsecureAllowUnencryptedAuth bool
|
|
||||||
|
|
||||||
// NoTLS directs go-xmpp to not use TLS initially to contact the server; instead, a plain old unencrypted
|
|
||||||
// TCP connection should be used. (Can be combined with StartTLS to support STARTTLS-based servers.)
|
|
||||||
NoTLS bool
|
|
||||||
|
|
||||||
// StartTLS directs go-xmpp to STARTTLS if the server supports it; go-xmpp will automatically STARTTLS
|
|
||||||
// if the server requires it regardless of this option.
|
|
||||||
StartTLS bool
|
|
||||||
|
|
||||||
// Debug output
|
|
||||||
Debug bool
|
|
||||||
|
|
||||||
// Use server sessions
|
|
||||||
Session bool
|
|
||||||
|
|
||||||
// Presence Status
|
|
||||||
Status string
|
|
||||||
|
|
||||||
// Status message
|
|
||||||
StatusMessage string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient establishes a new Client connection based on a set of Options.
|
|
||||||
func (o Options) NewClient() (*Client, error) {
|
|
||||||
host := o.Host
|
|
||||||
c, err := connect(host, o.User, o.Password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.LastIndex(o.Host, ":") > 0 {
|
|
||||||
host = host[:strings.LastIndex(o.Host, ":")]
|
|
||||||
}
|
|
||||||
|
|
||||||
client := new(Client)
|
|
||||||
if o.NoTLS {
|
|
||||||
client.conn = c
|
|
||||||
} else {
|
|
||||||
var tlsconn *tls.Conn
|
|
||||||
if o.TLSConfig != nil {
|
|
||||||
tlsconn = tls.Client(c, o.TLSConfig)
|
|
||||||
} else {
|
|
||||||
DefaultConfig.ServerName = host
|
|
||||||
newconfig := DefaultConfig
|
|
||||||
newconfig.ServerName = host
|
|
||||||
tlsconn = tls.Client(c, &newconfig)
|
|
||||||
}
|
|
||||||
if err = tlsconn.Handshake(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
insecureSkipVerify := DefaultConfig.InsecureSkipVerify
|
|
||||||
if o.TLSConfig != nil {
|
|
||||||
insecureSkipVerify = o.TLSConfig.InsecureSkipVerify
|
|
||||||
}
|
|
||||||
if !insecureSkipVerify {
|
|
||||||
if err = tlsconn.VerifyHostname(host); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.conn = tlsconn
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.init(&o); err != nil {
|
|
||||||
client.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient creates a new connection to a host given as "hostname" or "hostname:port".
|
|
||||||
// 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(host, user, passwd string, debug bool) (*Client, error) {
|
|
||||||
opts := Options{
|
|
||||||
Host: host,
|
|
||||||
User: user,
|
|
||||||
Password: passwd,
|
|
||||||
Debug: debug,
|
|
||||||
Session: false,
|
|
||||||
}
|
|
||||||
return opts.NewClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClientNoTLS creates a new client without TLS
|
|
||||||
func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) {
|
|
||||||
opts := Options{
|
|
||||||
Host: host,
|
|
||||||
User: user,
|
|
||||||
Password: passwd,
|
|
||||||
NoTLS: true,
|
|
||||||
Debug: debug,
|
|
||||||
Session: false,
|
|
||||||
}
|
|
||||||
return opts.NewClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the XMPP connection
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
if c.conn != (*tls.Conn)(nil) {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func saslDigestResponse(username, realm, passwd, nonce, cnonceStr, authenticate, digestURI, nonceCountStr string) string {
|
|
||||||
h := func(text string) []byte {
|
|
||||||
h := md5.New()
|
|
||||||
h.Write([]byte(text))
|
|
||||||
return h.Sum(nil)
|
|
||||||
}
|
|
||||||
hex := func(bytes []byte) string {
|
|
||||||
return fmt.Sprintf("%x", bytes)
|
|
||||||
}
|
|
||||||
kd := func(secret, data string) []byte {
|
|
||||||
return h(secret + ":" + data)
|
|
||||||
}
|
|
||||||
|
|
||||||
a1 := string(h(username+":"+realm+":"+passwd)) + ":" + nonce + ":" + cnonceStr
|
|
||||||
a2 := authenticate + ":" + digestURI
|
|
||||||
response := hex(kd(hex(h(a1)), nonce+":"+nonceCountStr+":"+cnonceStr+":auth:"+hex(h(a2))))
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
func cnonce() string {
|
|
||||||
randSize := big.NewInt(0)
|
|
||||||
randSize.Lsh(big.NewInt(1), 64)
|
|
||||||
cn, err := rand.Int(rand.Reader, randSize)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%016x", cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) init(o *Options) error {
|
|
||||||
|
|
||||||
var domain string
|
|
||||||
var user string
|
|
||||||
a := strings.SplitN(o.User, "@", 2)
|
|
||||||
if len(o.User) > 0 {
|
|
||||||
if len(a) != 2 {
|
|
||||||
return errors.New("xmpp: invalid username (want user@domain): " + o.User)
|
|
||||||
}
|
|
||||||
user = a[0]
|
|
||||||
domain = a[1]
|
|
||||||
} // Otherwise, we'll be attempting ANONYMOUS
|
|
||||||
|
|
||||||
// Declare intent to be a jabber client and gather stream features.
|
|
||||||
f, err := c.startStream(o, domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the server requires we STARTTLS, attempt to do so.
|
|
||||||
if f, err = c.startTLSIfRequired(f, o, domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.User == "" && o.Password == "" {
|
|
||||||
foundAnonymous := false
|
|
||||||
for _, m := range f.Mechanisms.Mechanism {
|
|
||||||
if m == "ANONYMOUS" {
|
|
||||||
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='ANONYMOUS' />\n", nsSASL)
|
|
||||||
foundAnonymous = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !foundAnonymous {
|
|
||||||
return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Even digest forms of authentication are unsafe if we do not know that the host
|
|
||||||
// we are talking to is the actual server, and not a man in the middle playing
|
|
||||||
// proxy.
|
|
||||||
if !c.IsEncrypted() && !o.InsecureAllowUnencryptedAuth {
|
|
||||||
return errors.New("refusing to authenticate over unencrypted TCP connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
mechanism := ""
|
|
||||||
for _, m := range f.Mechanisms.Mechanism {
|
|
||||||
if m == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" {
|
|
||||||
mechanism = m
|
|
||||||
// Oauth authentication: send base64-encoded \x00 user \x00 token.
|
|
||||||
raw := "\x00" + user + "\x00" + o.OAuthToken
|
|
||||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
|
||||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
|
||||||
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='X-OAUTH2' auth:service='oauth2' "+
|
|
||||||
"xmlns:auth='%s'>%s</auth>\n", nsSASL, o.OAuthXmlNs, enc)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if m == "PLAIN" {
|
|
||||||
mechanism = m
|
|
||||||
// Plain authentication: send base64-encoded \x00 user \x00 password.
|
|
||||||
raw := "\x00" + user + "\x00" + o.Password
|
|
||||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
|
||||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
|
||||||
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n", nsSASL, enc)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if m == "DIGEST-MD5" {
|
|
||||||
mechanism = m
|
|
||||||
// Digest-MD5 authentication
|
|
||||||
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='DIGEST-MD5'/>\n", nsSASL)
|
|
||||||
var ch saslChallenge
|
|
||||||
if err = c.p.DecodeElement(&ch, nil); err != nil {
|
|
||||||
return errors.New("unmarshal <challenge>: " + err.Error())
|
|
||||||
}
|
|
||||||
b, err := base64.StdEncoding.DecodeString(string(ch))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tokens := map[string]string{}
|
|
||||||
for _, token := range strings.Split(string(b), ",") {
|
|
||||||
kv := strings.SplitN(strings.TrimSpace(token), "=", 2)
|
|
||||||
if len(kv) == 2 {
|
|
||||||
if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' {
|
|
||||||
kv[1] = kv[1][1 : len(kv[1])-1]
|
|
||||||
}
|
|
||||||
tokens[kv[0]] = kv[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
realm, _ := tokens["realm"]
|
|
||||||
nonce, _ := tokens["nonce"]
|
|
||||||
qop, _ := tokens["qop"]
|
|
||||||
charset, _ := tokens["charset"]
|
|
||||||
cnonceStr := cnonce()
|
|
||||||
digestURI := "xmpp/" + domain
|
|
||||||
nonceCount := fmt.Sprintf("%08x", 1)
|
|
||||||
digest := saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", digestURI, nonceCount)
|
|
||||||
message := "username=\"" + user + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", cnonce=\"" + cnonceStr +
|
|
||||||
"\", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=\"" + digestURI + "\", response=" + digest + ", charset=" + charset
|
|
||||||
|
|
||||||
fmt.Fprintf(c.conn, "<response xmlns='%s'>%s</response>\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message)))
|
|
||||||
|
|
||||||
var rspauth saslRspAuth
|
|
||||||
if err = c.p.DecodeElement(&rspauth, nil); err != nil {
|
|
||||||
return errors.New("unmarshal <challenge>: " + err.Error())
|
|
||||||
}
|
|
||||||
b, err = base64.StdEncoding.DecodeString(string(rspauth))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprintf(c.conn, "<response xmlns='%s'/>\n", nsSASL)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mechanism == "" {
|
|
||||||
return fmt.Errorf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Next message should be either success or failure.
|
|
||||||
name, val, err := next(c.p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch v := val.(type) {
|
|
||||||
case *saslSuccess:
|
|
||||||
case *saslFailure:
|
|
||||||
errorMessage := v.Text
|
|
||||||
if errorMessage == "" {
|
|
||||||
// v.Any is type of sub-element in failure,
|
|
||||||
// which gives a description of what failed if there was no text element
|
|
||||||
errorMessage = v.Any.Local
|
|
||||||
}
|
|
||||||
return errors.New("auth failure: " + errorMessage)
|
|
||||||
default:
|
|
||||||
return errors.New("expected <success> or <failure>, got <" + name.Local + "> in " + name.Space)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we're authenticated, we're supposed to start the stream over again.
|
|
||||||
// Declare intent to be a jabber client.
|
|
||||||
if f, err = c.startStream(o, domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a unique cookie
|
|
||||||
cookie := getCookie()
|
|
||||||
|
|
||||||
// Send IQ message asking to bind to the local user name.
|
|
||||||
if o.Resource == "" {
|
|
||||||
fmt.Fprintf(c.conn, "<iq type='set' id='%x'><bind xmlns='%s'></bind></iq>\n", cookie, nsBind)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(c.conn, "<iq type='set' id='%x'><bind xmlns='%s'><resource>%s</resource></bind></iq>\n", cookie, nsBind, o.Resource)
|
|
||||||
}
|
|
||||||
var iq clientIQ
|
|
||||||
if err = c.p.DecodeElement(&iq, nil); err != nil {
|
|
||||||
return errors.New("unmarshal <iq>: " + err.Error())
|
|
||||||
}
|
|
||||||
if &iq.Bind == nil {
|
|
||||||
return errors.New("<iq> result missing <bind>")
|
|
||||||
}
|
|
||||||
c.jid = iq.Bind.Jid // our local id
|
|
||||||
c.domain = domain
|
|
||||||
|
|
||||||
if o.Session {
|
|
||||||
//if server support session, open it
|
|
||||||
fmt.Fprintf(c.conn, "<iq to='%s' type='set' id='%x'><session xmlns='%s'/></iq>", xmlEscape(domain), cookie, nsSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're connected and can now receive and send messages.
|
|
||||||
fmt.Fprintf(c.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", o.Status, o.StatusMessage)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// startTlsIfRequired examines the server's stream features and, if STARTTLS is required or supported, performs the TLS handshake.
|
|
||||||
// f will be updated if the handshake completes, as the new stream's features are typically different from the original.
|
|
||||||
func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string) (*streamFeatures, error) {
|
|
||||||
// whether we start tls is a matter of opinion: the server's and the user's.
|
|
||||||
switch {
|
|
||||||
case f.StartTLS == nil:
|
|
||||||
// the server does not support STARTTLS
|
|
||||||
return f, nil
|
|
||||||
case f.StartTLS.Required != nil:
|
|
||||||
// the server requires STARTTLS.
|
|
||||||
case !o.StartTLS:
|
|
||||||
// the user wants STARTTLS and the server supports it.
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
fmt.Fprintf(c.conn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>\n")
|
|
||||||
var k tlsProceed
|
|
||||||
if err = c.p.DecodeElement(&k, nil); err != nil {
|
|
||||||
return f, errors.New("unmarshal <proceed>: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
tc := o.TLSConfig
|
|
||||||
if tc == nil {
|
|
||||||
tc = new(tls.Config)
|
|
||||||
*tc = DefaultConfig
|
|
||||||
//TODO(scott): we should consider using the server's address or reverse lookup
|
|
||||||
tc.ServerName = domain
|
|
||||||
}
|
|
||||||
t := tls.Client(c.conn, tc)
|
|
||||||
|
|
||||||
if err = t.Handshake(); err != nil {
|
|
||||||
return f, errors.New("starttls handshake: " + err.Error())
|
|
||||||
}
|
|
||||||
c.conn = t
|
|
||||||
|
|
||||||
// restart our declaration of XMPP stream intentions.
|
|
||||||
tf, err := c.startStream(o, domain)
|
|
||||||
if err != nil {
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
return tf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// startStream will start a new XML decoder for the connection, signal the start of a stream to the server and verify that the server has
|
|
||||||
// also started the stream; if o.Debug is true, startStream will tee decoded XML data to stderr. The features advertised by the server
|
|
||||||
// will be returned.
|
|
||||||
func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) {
|
|
||||||
if o.Debug {
|
|
||||||
c.p = xml.NewDecoder(tee{c.conn, os.Stderr})
|
|
||||||
} else {
|
|
||||||
c.p = xml.NewDecoder(c.conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := fmt.Fprintf(c.conn, "<?xml version='1.0'?>\n"+
|
|
||||||
"<stream:stream to='%s' xmlns='%s'\n"+
|
|
||||||
" xmlns:stream='%s' version='1.0'>\n",
|
|
||||||
xmlEscape(domain), nsClient, nsStream)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect the server to start a <stream>.
|
|
||||||
se, err := nextStart(c.p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if se.Name.Space != nsStream || se.Name.Local != "stream" {
|
|
||||||
return nil, fmt.Errorf("expected <stream> but got <%v> in %v", se.Name.Local, se.Name.Space)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we're in the stream and can use Unmarshal.
|
|
||||||
// Next message should be <features> to tell us authentication options.
|
|
||||||
// See section 4.6 in RFC 3920.
|
|
||||||
f := new(streamFeatures)
|
|
||||||
if err = c.p.DecodeElement(f, nil); err != nil {
|
|
||||||
return f, errors.New("unmarshal <features>: " + err.Error())
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEncrypted will return true if the client is connected using a TLS transport, either because it used.
|
|
||||||
// TLS to connect from the outset, or because it successfully used STARTTLS to promote a TCP connection to TLS.
|
|
||||||
func (c *Client) IsEncrypted() bool {
|
|
||||||
_, ok := c.conn.(*tls.Conn)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat is an incoming or outgoing XMPP chat message.
|
|
||||||
type Chat struct {
|
|
||||||
Remote string
|
|
||||||
Type string
|
|
||||||
Text string
|
|
||||||
Subject string
|
|
||||||
Thread string
|
|
||||||
Roster Roster
|
|
||||||
Other []string
|
|
||||||
OtherElem []XMLElement
|
|
||||||
Stamp time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Roster []Contact
|
|
||||||
|
|
||||||
type Contact struct {
|
|
||||||
Remote string
|
|
||||||
Name string
|
|
||||||
Group []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presence is an XMPP presence notification.
|
|
||||||
type Presence struct {
|
|
||||||
From string
|
|
||||||
To string
|
|
||||||
Type string
|
|
||||||
Show string
|
|
||||||
Status string
|
|
||||||
}
|
|
||||||
|
|
||||||
type IQ struct {
|
|
||||||
ID string
|
|
||||||
From string
|
|
||||||
To string
|
|
||||||
Type string
|
|
||||||
Query []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recv waits to receive the next XMPP stanza.
|
|
||||||
// Return type is either a presence notification or a chat message.
|
|
||||||
func (c *Client) Recv() (stanza interface{}, err error) {
|
|
||||||
for {
|
|
||||||
_, val, err := next(c.p)
|
|
||||||
if err != nil {
|
|
||||||
return Chat{}, err
|
|
||||||
}
|
|
||||||
switch v := val.(type) {
|
|
||||||
case *clientMessage:
|
|
||||||
stamp, _ := time.Parse(
|
|
||||||
"2006-01-02T15:04:05Z",
|
|
||||||
v.Delay.Stamp,
|
|
||||||
)
|
|
||||||
chat := Chat{
|
|
||||||
Remote: v.From,
|
|
||||||
Type: v.Type,
|
|
||||||
Text: v.Body,
|
|
||||||
Subject: v.Subject,
|
|
||||||
Thread: v.Thread,
|
|
||||||
Other: v.OtherStrings(),
|
|
||||||
OtherElem: v.Other,
|
|
||||||
Stamp: stamp,
|
|
||||||
}
|
|
||||||
return chat, nil
|
|
||||||
case *clientQuery:
|
|
||||||
var r Roster
|
|
||||||
for _, item := range v.Item {
|
|
||||||
r = append(r, Contact{item.Jid, item.Name, item.Group})
|
|
||||||
}
|
|
||||||
return Chat{Type: "roster", Roster: r}, nil
|
|
||||||
case *clientPresence:
|
|
||||||
return Presence{v.From, v.To, v.Type, v.Show, v.Status}, nil
|
|
||||||
case *clientIQ:
|
|
||||||
// TODO check more strictly
|
|
||||||
if bytes.Equal(bytes.TrimSpace(v.Query), []byte(`<ping xmlns='urn:xmpp:ping'/>`)) || bytes.Equal(bytes.TrimSpace(v.Query), []byte(`<ping xmlns="urn:xmpp:ping"/>`)) {
|
|
||||||
err := c.SendResultPing(v.ID, v.From)
|
|
||||||
if err != nil {
|
|
||||||
return Chat{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type, Query: v.Query}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send sends the message wrapped inside an XMPP message stanza body.
|
|
||||||
func (c *Client) Send(chat Chat) (n int, err error) {
|
|
||||||
var subtext = ``
|
|
||||||
var thdtext = ``
|
|
||||||
if chat.Subject != `` {
|
|
||||||
subtext = `<subject>` + xmlEscape(chat.Subject) + `</subject>`
|
|
||||||
}
|
|
||||||
if chat.Thread != `` {
|
|
||||||
thdtext = `<thread>` + xmlEscape(chat.Thread) + `</thread>`
|
|
||||||
}
|
|
||||||
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>" + subtext + "<body>%s</body>" + thdtext + "</message>",
|
|
||||||
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendOrg sends the original text without being wrapped in an XMPP message stanza.
|
|
||||||
func (c *Client) SendOrg(org string) (n int, err error) {
|
|
||||||
return fmt.Fprint(c.conn, org)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SendPresence(presence Presence) (n int, err error) {
|
|
||||||
return fmt.Fprintf(c.conn, "<presence from='%s' to='%s'/>", xmlEscape(presence.From), xmlEscape(presence.To))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendKeepAlive sends a "whitespace keepalive" as described in chapter 4.6.1 of RFC6120.
|
|
||||||
func (c *Client) SendKeepAlive() (n int, err error) {
|
|
||||||
return fmt.Fprintf(c.conn, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendHtml sends the message as HTML as defined by XEP-0071
|
|
||||||
func (c *Client) SendHtml(chat Chat) (n int, err error) {
|
|
||||||
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+
|
|
||||||
"<body>%s</body>"+
|
|
||||||
"<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>%s</body></html></message>",
|
|
||||||
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), chat.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roster asks for the chat roster.
|
|
||||||
func (c *Client) Roster() error {
|
|
||||||
fmt.Fprintf(c.conn, "<iq from='%s' type='get' id='roster1'><query xmlns='jabber:iq:roster'/></iq>\n", xmlEscape(c.jid))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 3920 C.1 Streams name space
|
|
||||||
type streamFeatures struct {
|
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
|
||||||
StartTLS *tlsStartTLS
|
|
||||||
Mechanisms saslMechanisms
|
|
||||||
Bind bindBind
|
|
||||||
Session bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamError struct {
|
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
|
|
||||||
Any xml.Name
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 3920 C.3 TLS name space
|
|
||||||
type tlsStartTLS struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
|
||||||
Required *string `xml:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 3920 C.4 SASL name space
|
|
||||||
type saslMechanisms struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
|
||||||
Mechanism []string `xml:"mechanism"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type saslAuth struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
|
||||||
Mechanism string `xml:",attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type saslChallenge string
|
|
||||||
|
|
||||||
type saslRspAuth string
|
|
||||||
|
|
||||||
type saslResponse string
|
|
||||||
|
|
||||||
type saslAbort struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl abort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type saslSuccess struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type saslFailure struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
|
||||||
Any xml.Name `xml:",any"`
|
|
||||||
Text string `xml:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 3920 C.5 Resource binding name space
|
|
||||||
type bindBind struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
|
||||||
Resource string
|
|
||||||
Jid string `xml:"jid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 3921 B.1 jabber:client
|
|
||||||
type clientMessage struct {
|
|
||||||
XMLName xml.Name `xml:"jabber:client message"`
|
|
||||||
From string `xml:"from,attr"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
To string `xml:"to,attr"`
|
|
||||||
Type string `xml:"type,attr"` // chat, error, groupchat, headline, or normal
|
|
||||||
|
|
||||||
// These should technically be []clientText, but string is much more convenient.
|
|
||||||
Subject string `xml:"subject"`
|
|
||||||
Body string `xml:"body"`
|
|
||||||
Thread string `xml:"thread"`
|
|
||||||
|
|
||||||
// Any hasn't matched element
|
|
||||||
Other []XMLElement `xml:",any"`
|
|
||||||
|
|
||||||
Delay Delay `xml:"delay"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *clientMessage) OtherStrings() []string {
|
|
||||||
a := make([]string, len(m.Other))
|
|
||||||
for i, e := range m.Other {
|
|
||||||
a[i] = e.String()
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
type XMLElement struct {
|
|
||||||
XMLName xml.Name
|
|
||||||
InnerXML string `xml:",innerxml"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *XMLElement) String() string {
|
|
||||||
r := bytes.NewReader([]byte(e.InnerXML))
|
|
||||||
d := xml.NewDecoder(r)
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for {
|
|
||||||
tok, err := d.Token()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch v := tok.(type) {
|
|
||||||
case xml.StartElement:
|
|
||||||
err = d.Skip()
|
|
||||||
case xml.CharData:
|
|
||||||
_, err = buf.Write(v)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Delay struct {
|
|
||||||
Stamp string `xml:"stamp,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientText struct {
|
|
||||||
Lang string `xml:",attr"`
|
|
||||||
Body string `xml:"chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientPresence struct {
|
|
||||||
XMLName xml.Name `xml:"jabber:client presence"`
|
|
||||||
From string `xml:"from,attr"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
To string `xml:"to,attr"`
|
|
||||||
Type string `xml:"type,attr"` // error, probe, subscribe, subscribed, unavailable, unsubscribe, unsubscribed
|
|
||||||
Lang string `xml:"lang,attr"`
|
|
||||||
|
|
||||||
Show string `xml:"show"` // away, chat, dnd, xa
|
|
||||||
Status string `xml:"status"` // sb []clientText
|
|
||||||
Priority string `xml:"priority,attr"`
|
|
||||||
Error *clientError
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientIQ struct { // info/query
|
|
||||||
XMLName xml.Name `xml:"jabber:client iq"`
|
|
||||||
From string `xml:"from,attr"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
To string `xml:"to,attr"`
|
|
||||||
Type string `xml:"type,attr"` // error, get, result, set
|
|
||||||
Query []byte `xml:",innerxml"`
|
|
||||||
Error clientError
|
|
||||||
Bind bindBind
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientError struct {
|
|
||||||
XMLName xml.Name `xml:"jabber:client error"`
|
|
||||||
Code string `xml:",attr"`
|
|
||||||
Type string `xml:",attr"`
|
|
||||||
Any xml.Name
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientQuery struct {
|
|
||||||
Item []rosterItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type rosterItem struct {
|
|
||||||
XMLName xml.Name `xml:"jabber:iq:roster item"`
|
|
||||||
Jid string `xml:",attr"`
|
|
||||||
Name string `xml:",attr"`
|
|
||||||
Subscription string `xml:",attr"`
|
|
||||||
Group []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan XML token stream to find next StartElement.
|
|
||||||
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
|
||||||
for {
|
|
||||||
t, err := p.Token()
|
|
||||||
if err != nil || t == nil {
|
|
||||||
return xml.StartElement{}, err
|
|
||||||
}
|
|
||||||
switch t := t.(type) {
|
|
||||||
case xml.StartElement:
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan XML token stream for next element and save into val.
|
|
||||||
// If val == nil, allocate new element based on proto map.
|
|
||||||
// Either way, return val.
|
|
||||||
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
|
|
||||||
// Read start element to find out what type we want.
|
|
||||||
se, err := nextStart(p)
|
|
||||||
if err != nil {
|
|
||||||
return xml.Name{}, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put it in an interface and allocate one.
|
|
||||||
var nv interface{}
|
|
||||||
switch se.Name.Space + " " + se.Name.Local {
|
|
||||||
case nsStream + " features":
|
|
||||||
nv = &streamFeatures{}
|
|
||||||
case nsStream + " error":
|
|
||||||
nv = &streamError{}
|
|
||||||
case nsTLS + " starttls":
|
|
||||||
nv = &tlsStartTLS{}
|
|
||||||
case nsTLS + " proceed":
|
|
||||||
nv = &tlsProceed{}
|
|
||||||
case nsTLS + " failure":
|
|
||||||
nv = &tlsFailure{}
|
|
||||||
case nsSASL + " mechanisms":
|
|
||||||
nv = &saslMechanisms{}
|
|
||||||
case nsSASL + " challenge":
|
|
||||||
nv = ""
|
|
||||||
case nsSASL + " response":
|
|
||||||
nv = ""
|
|
||||||
case nsSASL + " abort":
|
|
||||||
nv = &saslAbort{}
|
|
||||||
case nsSASL + " success":
|
|
||||||
nv = &saslSuccess{}
|
|
||||||
case nsSASL + " failure":
|
|
||||||
nv = &saslFailure{}
|
|
||||||
case nsBind + " bind":
|
|
||||||
nv = &bindBind{}
|
|
||||||
case nsClient + " message":
|
|
||||||
nv = &clientMessage{}
|
|
||||||
case nsClient + " presence":
|
|
||||||
nv = &clientPresence{}
|
|
||||||
case nsClient + " iq":
|
|
||||||
nv = &clientIQ{}
|
|
||||||
case nsClient + " error":
|
|
||||||
nv = &clientError{}
|
|
||||||
default:
|
|
||||||
return xml.Name{}, nil, errors.New("unexpected XMPP message " +
|
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal into that storage.
|
|
||||||
if err = p.DecodeElement(nv, &se); err != nil {
|
|
||||||
return xml.Name{}, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return se.Name, nv, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func xmlEscape(s string) string {
|
|
||||||
var b bytes.Buffer
|
|
||||||
xml.Escape(&b, []byte(s))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type tee struct {
|
|
||||||
r io.Reader
|
|
||||||
w io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t tee) Read(p []byte) (n int, err error) {
|
|
||||||
n, err = t.r.Read(p)
|
|
||||||
if n > 0 {
|
|
||||||
t.w.Write(p[0:n])
|
|
||||||
t.w.Write([]byte("\n"))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
const IQTypeGet = "get"
|
|
||||||
const IQTypeSet = "set"
|
|
||||||
const IQTypeResult = "result"
|
|
||||||
|
|
||||||
func (c *Client) Discovery() (string, error) {
|
|
||||||
const namespace = "http://jabber.org/protocol/disco#items"
|
|
||||||
// use getCookie for a pseudo random id.
|
|
||||||
reqID := strconv.FormatUint(uint64(getCookie()), 10)
|
|
||||||
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, namespace, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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>"
|
|
||||||
_, err := fmt.Fprintf(c.conn, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// rawInformation send a IQ request with the 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>"
|
|
||||||
_, err := fmt.Fprintf(c.conn, 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 (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
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.conn, "<message to='%s' type='%s' xml:lang='en'>"+"<subject>%s</subject></message>",
|
|
||||||
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.conn, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>"+
|
|
||||||
"<history maxchars='0'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
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.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s' />\n" +
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
|
||||||
case CharHistory:
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<history maxchars='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
|
||||||
case StanzaHistory:
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<history maxstanzas='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
|
||||||
case SecondsHistory:
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<history seconds='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
|
||||||
case SinceHistory:
|
|
||||||
if history_date != nil {
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<history since='%s'/></x>\n" +
|
|
||||||
"</presence>",
|
|
||||||
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.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<password>%s</password>" +
|
|
||||||
"</x>\n" +
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
|
|
||||||
case CharHistory:
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history maxchars='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
|
||||||
case StanzaHistory:
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history maxstanzas='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
|
||||||
case SecondsHistory:
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history seconds='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
|
||||||
case SinceHistory:
|
|
||||||
if history_date != nil {
|
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
|
||||||
"<x xmlns='%s'>\n" +
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history since='%s'/></x>\n" +
|
|
||||||
"</presence>",
|
|
||||||
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.conn, "<presence from='%s' to='%s' type='unavailable' />",
|
|
||||||
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.conn, "<iq from='%s' to='%s' id='c2s1' type='get'>\n"+
|
|
||||||
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
|
||||||
"</iq>",
|
|
||||||
xmlEscape(jid), xmlEscape(server))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) PingS2S(fromServer, toServer string) error {
|
|
||||||
_, err := fmt.Fprintf(c.conn, "<iq from='%s' to='%s' id='s2s1' type='get'>\n"+
|
|
||||||
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
|
||||||
"</iq>",
|
|
||||||
xmlEscape(fromServer), xmlEscape(toServer))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SendResultPing(id, toServer string) error {
|
|
||||||
_, err := fmt.Fprintf(c.conn, "<iq type='result' to='%s' id='%s'/>",
|
|
||||||
xmlEscape(toServer), xmlEscape(id))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) ApproveSubscription(jid string) {
|
|
||||||
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribed'/>",
|
|
||||||
xmlEscape(jid))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) RevokeSubscription(jid string) {
|
|
||||||
fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribed'/>",
|
|
||||||
xmlEscape(jid))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) RequestSubscription(jid string) {
|
|
||||||
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribe'/>",
|
|
||||||
xmlEscape(jid))
|
|
||||||
}
|
|
||||||
-116
@@ -1,116 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/xml"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type localAddr struct{}
|
|
||||||
|
|
||||||
func (a *localAddr) Network() string {
|
|
||||||
return "tcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (addr *localAddr) String() string {
|
|
||||||
return "localhost:5222"
|
|
||||||
}
|
|
||||||
|
|
||||||
type testConn struct {
|
|
||||||
*bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func tConnect(s string) net.Conn {
|
|
||||||
var conn testConn
|
|
||||||
conn.Buffer = bytes.NewBufferString(s)
|
|
||||||
return &conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testConn) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testConn) LocalAddr() net.Addr {
|
|
||||||
return &localAddr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testConn) RemoteAddr() net.Addr {
|
|
||||||
return &localAddr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testConn) SetDeadline(time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testConn) SetReadDeadline(time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testConn) SetWriteDeadline(time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = strings.TrimSpace(`
|
|
||||||
<message xmlns="jabber:client" id="3" type="error" to="123456789@gcm.googleapis.com/ABC">
|
|
||||||
<gcm xmlns="google:mobile:data">
|
|
||||||
{"random": "<text>"}
|
|
||||||
</gcm>
|
|
||||||
<error code="400" type="modify">
|
|
||||||
<bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
|
|
||||||
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
|
|
||||||
InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n
|
|
||||||
</text>
|
|
||||||
</error>
|
|
||||||
</message>
|
|
||||||
`)
|
|
||||||
|
|
||||||
func TestStanzaError(t *testing.T) {
|
|
||||||
var c Client
|
|
||||||
c.conn = tConnect(text)
|
|
||||||
c.p = xml.NewDecoder(c.conn)
|
|
||||||
v, err := c.Recv()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Recv() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chat := Chat{
|
|
||||||
Type: "error",
|
|
||||||
Other: []string{
|
|
||||||
"\n\t\t{\"random\": \"<text>\"}\n\t",
|
|
||||||
"\n\t\t\n\t\t\n\t",
|
|
||||||
},
|
|
||||||
OtherElem: []XMLElement{
|
|
||||||
XMLElement{
|
|
||||||
XMLName: xml.Name{Space: "google:mobile:data", Local: "gcm"},
|
|
||||||
InnerXML: "\n\t\t{\"random\": \"<text>\"}\n\t",
|
|
||||||
},
|
|
||||||
XMLElement{
|
|
||||||
XMLName: xml.Name{Space: "jabber:client", Local: "error"},
|
|
||||||
InnerXML: `
|
|
||||||
<bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
|
|
||||||
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
|
|
||||||
InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n
|
|
||||||
</text>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(v, chat) {
|
|
||||||
t.Errorf("Recv() = %#v; want %#v", v, chat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEOFError(t *testing.T) {
|
|
||||||
var c Client
|
|
||||||
c.conn = tConnect("")
|
|
||||||
c.p = xml.NewDecoder(c.conn)
|
|
||||||
_, err := c.Recv()
|
|
||||||
if err != io.EOF {
|
|
||||||
t.Errorf("Recv() did not return io.EOF on end of input stream")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user