mirror of
https://github.com/FluuxIO/go-xmpp.git
synced 2026-05-22 20:13:46 -07:00
Compare commits
294 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
370c500a5e
|
|||
| 7154bfeb76 | |||
| 243a438354 | |||
| e9123cc4b3 | |||
| 4be597a84a | |||
| 464fbe04ef | |||
| e223dcf94b | |||
| 321c2b14a5 | |||
| 9161feef4d | |||
| fc3ed9a0b8 | |||
| d9df620fa4 | |||
| f067814851 | |||
| 961b7e435e | |||
| 12a04e0950 | |||
| 2f331ed19c | |||
| b0f55a8f7f | |||
| 7486b7a363 | |||
| da2377ecb0 | |||
| 44095406a2 | |||
| d7aee6b636 | |||
| 0324b31f56 | |||
| ca4e49201e | |||
| 6e5d6e449e | |||
| 0ae62a33a2 | |||
| ce687243c1 | |||
| 78d07e9eee | |||
| 416bb6e7b7 | |||
| aef1257ed1 | |||
| da17a46e6f | |||
| eedd7259cb | |||
| 07196efcf3 | |||
| bbd90cc04b | |||
| 0c7ee22452 | |||
| 862c21f845 | |||
| bc81053dbc | |||
| 94ab540b80 | |||
| f6a9836fdf | |||
| 8ab32d885f | |||
| 73f06c9f3d | |||
| 9c5e758356 | |||
| ea4874e8c9 | |||
| dab6865bd2 | |||
| c051d69509 | |||
| aed021cf3e | |||
| 2c4708e724 | |||
| 746409f074 | |||
| 9684a8ff69 | |||
| b7ea9f4be1 | |||
| e2bc7bf6d7 | |||
| 0bcc057225 | |||
| 49054ca9e9 | |||
| b369b7df10 | |||
| cc481e54e7 | |||
| 88855eac82 | |||
| 0cc0a72c15 | |||
| d6e9a15f29 | |||
| 6ffd595a06 | |||
| 62928b3483 | |||
| d67787ca0f | |||
| f8a24505f4 | |||
| 685570cbd8 | |||
| 7bfa331758 | |||
| 3f0cbac307 | |||
| 7ccad52e63 | |||
| 705f68d1a5 | |||
| b49bdce100 | |||
| f4c732fdc7 | |||
| d3d16d5db9 | |||
| 34d683d25a | |||
| dffa92c129 | |||
| 8531e2e36a | |||
| e7d5b17113 | |||
| c1b9689e75 | |||
| 424970d23c | |||
| 5fdcf18a81 | |||
| 794ed98f9f | |||
| 2f9bd427e8 | |||
| 70c2fe6900 | |||
| 39f5b80375 | |||
| 2449f4192b | |||
| 3462085098 | |||
| 6c9243326e | |||
| 31c7eb6919 | |||
| 9dcf67c0ad | |||
| 4c385a334c | |||
| 24e0f536cb | |||
| a6b124c9b2 | |||
| 6138e9dbe5 | |||
| 98ff0d4df7 | |||
| bef3e549f7 | |||
| 9129a110df | |||
| d72a0f3154 | |||
| 9fc0b1236c | |||
| 05cd75074a | |||
| 369824c83a | |||
| 2eb234970c | |||
| 3b26f73300 | |||
| 1411b9cc8b | |||
| 99ddfc1aa4 | |||
| e773596ea0 | |||
| 912ba61489 | |||
| 3871461df9 | |||
| db1339b3a5 | |||
| b40e129499 | |||
| 42ee290fc5 | |||
| da2b7586cd | |||
| 37fa6ef92f | |||
| 899ef71e80 | |||
| 3e4868bd3e | |||
| a86b6abcb3 | |||
| ac4c216a42 | |||
| 6093f50721 | |||
| 1f614e5b8d | |||
| ef6a1a617c | |||
| 65fd08aee2 | |||
| a79a0e59ef | |||
| 5709ddefa8 | |||
| 51b558cd2c | |||
| 66c008d798 | |||
| 224305b3ef | |||
| 1e7b50b41c | |||
| c18873b880 | |||
| 2c5079ea28 | |||
| 113d9c0420 | |||
| e543ad3fcd | |||
| 4fdbee9ac5 | |||
| 8a5843171f | |||
| 04ea54f191 | |||
| 7ec2b8b7de | |||
| 43e817cebe | |||
| 66aade104d | |||
| 2271ce0aec | |||
| fda8e5cb42 | |||
| bd84bf7b04 | |||
| 44c76a8761 | |||
| 3e4f4a3a80 | |||
| d0cdb99fae | |||
| 127e75bc8b | |||
| e69bd697cb | |||
| 2f138678c0 | |||
| e015f92cdf | |||
| fe382e4805 | |||
| 7ec8e81ec3 | |||
| f3cf3c3b40 | |||
| 906d9d747d | |||
| 16b6a7bdba | |||
| a74ec7bb2d | |||
| ac40267866 | |||
| 1610c524f7 | |||
| 0fe2a76e77 | |||
| 325c112042 | |||
| 18cda4524c | |||
| f4550b5399 | |||
| f66ee47cd9 | |||
| 02db6f5ed6 | |||
| 62f9ce3246 | |||
| 6265286138 | |||
| ccac8addc9 | |||
| e44d1877bb | |||
| c7af92b53b | |||
| aeb80ddc4d | |||
| bacbdeb205 | |||
| 12d5633a9d | |||
| 0948d88dae | |||
| e3871c2deb | |||
| 7c0791141b | |||
| 54cdc20727 | |||
| 0e6327115f | |||
| e810b2faca | |||
| adbceb5dae | |||
| c84fc9afab | |||
| 7d83a73298 | |||
| 5197953ad4 | |||
| 089ebf9bad | |||
| 9df9a5b5f9 | |||
| 9aeb3722bf | |||
| 188e3f03c7 | |||
| d86062634d | |||
| 637503f492 | |||
| 6618fc47ca | |||
| 84b9ced4e9 | |||
| 88f429802e | |||
| 222c8f8fd0 | |||
| 5f7c3b14b0 | |||
| b5c8af17a7 | |||
| 02e423485e | |||
| c8c5371616 | |||
| 0c0c98633c | |||
| 861872c8db | |||
| 404638fb3d | |||
| 874e70e091 | |||
| 9c349bcc3f | |||
| 6c1f4b23f8 | |||
| a1c1069091 | |||
| e8c25dcffe | |||
| cc56ae0810 | |||
| 9dd92e1247 | |||
| 09fb80afad | |||
| 8b13d0ad77 | |||
| 70ac466680 | |||
| 37dcc8bfca | |||
| 58077b314a | |||
| 4e8e43b7ca | |||
| 050bbf66bd | |||
| 0d259f5448 | |||
| c0f9b41b3d | |||
| 0655f5913b | |||
| 0a3375a6ad | |||
| a60980a550 | |||
| 98a0431d5b | |||
| 7568e71728 | |||
| 846b8175da | |||
| 477ccf01f6 | |||
| 15ac96c029 | |||
| d03bc801da | |||
| 9fe31adf02 | |||
| 61f20ce1de | |||
| 08299587ec | |||
| 1ff5be0d01 | |||
| 7a8cf41551 | |||
| 1f559fafde | |||
| ebd519cbfe | |||
| ac5d015101 | |||
| aa9390a115 | |||
| c9bbe151b2 | |||
| f402673c8c | |||
| 748282a14a | |||
| 15989a19c3 | |||
| aa27e3ee45 | |||
| 91047d400c | |||
| 11887e6acb | |||
| f06f19e121 | |||
| 41fd432f88 | |||
| 0fd114068f | |||
| 9276abaad9 | |||
| 8a08b956bb | |||
| f467ba7632 | |||
| e71f933d7c | |||
| 8da045a9e5 | |||
| 8a80c8abe3 | |||
| bb0e2d84ea | |||
| b67dc40516 | |||
| fd49820bf5 | |||
| 64821d5df9 | |||
| cbdf478ba7 | |||
| 73e63850ab | |||
| ba140e5eb7 | |||
| 12d98ae2dc | |||
| c88c22763a | |||
| af110491a0 | |||
| c3ac597871 | |||
| 700abb7449 | |||
| 7c9260e5a0 | |||
| 5e57ac52f9 | |||
| 52f561e157 | |||
| 4a4cac6dfc | |||
| 26e35d5504 | |||
| 53846d1e34 | |||
| df87efc875 | |||
| 75752790eb | |||
| 52c3f1b710 | |||
| 1e9dc674d1 | |||
| 99516ec31f | |||
| df2ef04578 | |||
| d92790f748 | |||
| cb6591b513 | |||
| 1dd8c2eeac | |||
| a25b82d0a4 | |||
| e4e8b7448c | |||
| e7715f9f21 | |||
| 2ec48a2db6 | |||
| f8ce0ead60 | |||
| f482858a52 | |||
| 4201b13e32 | |||
| 0a0f20b95e | |||
| b6a67e2320 | |||
| 4ddb93ef9d | |||
| fc3904b3a3 | |||
| 514bb10a8e | |||
| 7f4668977b | |||
| 986d7a0046 | |||
| 2644d2a47e | |||
| c922a1691f | |||
| 19a383f5b4 | |||
| 17fe2a046a | |||
| 094f05fb75 | |||
| 7ff519ae8c | |||
| fca5966193 | |||
| c3beae4a4e | |||
| aaaea96dd8 | |||
| 73126a5d4b | |||
| 08d0d26b2d | |||
| d5ee92e89e | |||
| 76fbc3bb63 |
-34
@@ -1,34 +0,0 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
*.test
|
|
||||||
*.prof
|
|
||||||
coverage.out
|
|
||||||
coverage.txt
|
|
||||||
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Do not commit codeship key
|
|
||||||
codeship.aes
|
|
||||||
codeship.env
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
script:
|
||||||
|
- go test
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community. Examples of
|
|
||||||
representing a project or community include using an official project e-mail
|
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported by contacting the project team at contact@process-one.net. All
|
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
|
||||||
members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see
|
|
||||||
https://www.contributor-covenant.org/faq
|
|
||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
We'd love for you to contribute to our source code and to make our project even better than it is
|
|
||||||
today! Here are the guidelines we'd like you to follow:
|
|
||||||
|
|
||||||
* [Code of Conduct](#coc)
|
|
||||||
* [Questions and Problems](#question)
|
|
||||||
* [Issues and Bugs](#issue)
|
|
||||||
* [Feature Requests](#feature)
|
|
||||||
* [Issue Submission Guidelines](#submit)
|
|
||||||
* [Pull Request Submission Guidelines](#submit-pr)
|
|
||||||
* [Signing the CLA](#cla)
|
|
||||||
|
|
||||||
## <a name="coc"></a> Code of Conduct
|
|
||||||
|
|
||||||
Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc].
|
|
||||||
|
|
||||||
## <a name="requests"></a> Questions, Bugs, Features
|
|
||||||
|
|
||||||
### <a name="question"></a> Got a Question or Problem?
|
|
||||||
|
|
||||||
Do not open issues for general support questions as we want to keep GitHub issues for bug reports
|
|
||||||
and feature requests. You've got much better chances of getting your question answered on dedicated
|
|
||||||
support platforms, the best being [Stack Overflow][stackoverflow].
|
|
||||||
|
|
||||||
Stack Overflow is a much better place to ask questions since:
|
|
||||||
|
|
||||||
- there are thousands of people willing to help on Stack Overflow
|
|
||||||
- questions and answers stay available for public viewing so your question / answer might help
|
|
||||||
someone else
|
|
||||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
|
||||||
|
|
||||||
To save your and our time, we will systematically close all issues that are requests for general
|
|
||||||
support and redirect people to the section you are reading right now.
|
|
||||||
|
|
||||||
### <a name="issue"></a> Found an Issue or Bug?
|
|
||||||
|
|
||||||
If you find a bug in the source code, you can help us by submitting an issue to our
|
|
||||||
[GitHub Repository][github]. Even better, you can submit a Pull Request with a fix.
|
|
||||||
|
|
||||||
### <a name="feature"></a> Missing a Feature?
|
|
||||||
|
|
||||||
You can request a new feature by submitting an issue to our [GitHub Repository][github-issues].
|
|
||||||
|
|
||||||
If you would like to implement a new feature then consider what kind of change it is:
|
|
||||||
|
|
||||||
* **Major Changes** that you wish to contribute to the project should be discussed first in an
|
|
||||||
[GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature.
|
|
||||||
* **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github]
|
|
||||||
as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr).
|
|
||||||
|
|
||||||
## <a name="submit"></a> Issue Submission Guidelines
|
|
||||||
|
|
||||||
Before you submit your issue search the archive, maybe your question was already answered.
|
|
||||||
|
|
||||||
If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize
|
|
||||||
the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
|
|
||||||
|
|
||||||
The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to
|
|
||||||
make it easier to understand and categorize the issue.
|
|
||||||
|
|
||||||
## <a name="submit-pr"></a> Pull Request Submission Guidelines
|
|
||||||
|
|
||||||
By submitting a pull request for a code or doc contribution, you need to have the right
|
|
||||||
to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla]
|
|
||||||
for details.
|
|
||||||
|
|
||||||
Before you submit your pull request consider the following guidelines:
|
|
||||||
|
|
||||||
* Search [GitHub][github-pr] for an open or closed Pull Request
|
|
||||||
that relates to your submission. You don't want to duplicate effort.
|
|
||||||
* Make your changes in a new git branch:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git checkout -b my-fix-branch master
|
|
||||||
```
|
|
||||||
* Test your changes and, if relevant, expand the automated test suite.
|
|
||||||
* Create your patch commit, including appropriate test cases.
|
|
||||||
* If the changes affect public APIs, change or add relevant documentation.
|
|
||||||
* Commit your changes using a descriptive commit message.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git commit -a
|
|
||||||
```
|
|
||||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
|
||||||
|
|
||||||
* Push your branch to GitHub:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git push origin my-fix-branch
|
|
||||||
```
|
|
||||||
|
|
||||||
* In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test.
|
|
||||||
We will also notify you if you have not yet signed the [contribution agreement][cla].
|
|
||||||
|
|
||||||
* If you find that the continunous integration has failed, look into the logs to find out
|
|
||||||
if your changes caused test failures, the commit message was malformed etc. If you find that the
|
|
||||||
tests failed or times out for unrelated reasons, you can ping a team member so that the build can be
|
|
||||||
restarted.
|
|
||||||
|
|
||||||
* If we suggest changes, then:
|
|
||||||
|
|
||||||
* Make the required updates.
|
|
||||||
* Test your changes and test cases.
|
|
||||||
* Commit your changes to your branch (e.g. `my-fix-branch`).
|
|
||||||
* Push the changes to your GitHub repository (this will update your Pull Request).
|
|
||||||
|
|
||||||
You can also amend the initial commits and force push them to the branch.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git rebase master -i
|
|
||||||
git push origin my-fix-branch -f
|
|
||||||
```
|
|
||||||
|
|
||||||
This is generally easier to follow, but separate commits are useful if the Pull Request contains
|
|
||||||
iterations that might be interesting to see side-by-side.
|
|
||||||
|
|
||||||
That's it! Thank you for your contribution!
|
|
||||||
|
|
||||||
## <a name="cla"></a> Signing the Contributor License Agreement (CLA)
|
|
||||||
|
|
||||||
Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done
|
|
||||||
so before. It's a quick process, we promise, and you will be able to do it all online
|
|
||||||
|
|
||||||
You can read [ProcessOne Contribution License Agreement][cla] in PDF.
|
|
||||||
|
|
||||||
This is part of the legal framework of the open-source ecosystem that adds some red tape,
|
|
||||||
but protects both the contributor and the company / foundation behind the project. It also
|
|
||||||
gives us the option to relicense the code with a more permissive license in the future.
|
|
||||||
|
|
||||||
|
|
||||||
[coc]: https://github.com/FluuxIO/go-xmpp/blob/master/CODE_OF_CONDUCT.md
|
|
||||||
[stackoverflow]: https://stackoverflow.com/
|
|
||||||
[github]: https://github.com/FluuxIO/go-xmpp
|
|
||||||
[github-issues]: https://github.com/FluuxIO/go-xmpp/issues
|
|
||||||
[github-new-issue]: https://github.com/FluuxIO/go-xmpp/issues/new
|
|
||||||
[github-pr]: https://github.com/FluuxIO/go-xmpp/pulls
|
|
||||||
[cla]: https://www.process-one.net/resources/ejabberd-cla.pdf
|
|
||||||
[license]: https://github.com/FluuxIO/go-xmpp/blob/master/LICENSE
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
FROM golang:1.12
|
|
||||||
WORKDIR /xmpp
|
|
||||||
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
|
|
||||||
COPY . ./
|
|
||||||
@@ -1,29 +1,27 @@
|
|||||||
BSD 3-Clause License
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
Copyright (c) 2017, ProcessOne SARL
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are met:
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
* Redistributions of source code must retain the above copyright
|
||||||
list of conditions and the following disclaimer.
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
this list of conditions and the following disclaimer in the documentation
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
and/or other materials provided with the distribution.
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
* Neither the name of the copyright holder nor the names of its
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
contributors may be used to endorse or promote products derived from
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
this software without specific prior written permission.
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|||||||
@@ -1,69 +1,6 @@
|
|||||||
# Fluux XMPP
|
go-xmpp
|
||||||
|
=======
|
||||||
|
|
||||||
[](https://app.codeship.com/projects/262399) [](https://godoc.org/gosrc.io/xmpp) [](https://goreportcard.com/report/fluux.io/xmpp) [](https://codecov.io/gh/FluuxIO/go-xmpp)
|
go xmpp library (original was written by russ cox )
|
||||||
|
|
||||||
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
[Documentation](https://godoc.org/github.com/xmppo/go-xmpp)
|
||||||
|
|
||||||
The goal is to make simple to write simple XMPP clients and components:
|
|
||||||
|
|
||||||
- For automation (like for example monitoring of an XMPP service),
|
|
||||||
- For building connected "things" by plugging them on an XMPP server,
|
|
||||||
- For writing simple chatbot to control a service or a thing.
|
|
||||||
- For writing XMPP servers components (See [XEP-0114](https://xmpp.org/extensions/xep-0114.html))
|
|
||||||
|
|
||||||
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
Here is a demo "echo" client:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
config := xmpp.Config{
|
|
||||||
Address: "localhost:5222",
|
|
||||||
Jid: "test@localhost",
|
|
||||||
Password: "test",
|
|
||||||
PacketLogger: os.Stdout,
|
|
||||||
Insecure: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config)
|
|
||||||
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.NewClientManager(client, nil)
|
|
||||||
err = cm.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterator to receive packets coming from our XMPP connection
|
|
||||||
for packet := range client.Recv() {
|
|
||||||
switch packet := packet.(type) {
|
|
||||||
case xmpp.Message:
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
|
|
||||||
reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body}
|
|
||||||
_ = client.Send(reply)
|
|
||||||
default:
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Please, check GoDoc for more information: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/matterbridge/go-xmpp"
|
||||||
|
"github.com/mattn/go-gtk/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
gtk.Init(&os.Args)
|
||||||
|
|
||||||
|
window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL)
|
||||||
|
window.SetTitle("GoTalk")
|
||||||
|
window.Connect("destroy", func() {
|
||||||
|
gtk.MainQuit()
|
||||||
|
})
|
||||||
|
vbox := gtk.NewVBox(false, 1)
|
||||||
|
scrolledwin := gtk.NewScrolledWindow(nil, nil)
|
||||||
|
textview := gtk.NewTextView()
|
||||||
|
textview.SetEditable(false)
|
||||||
|
textview.SetCursorVisible(false)
|
||||||
|
scrolledwin.Add(textview)
|
||||||
|
vbox.Add(scrolledwin)
|
||||||
|
|
||||||
|
buffer := textview.GetBuffer()
|
||||||
|
|
||||||
|
entry := gtk.NewEntry()
|
||||||
|
vbox.PackEnd(entry, false, false, 0)
|
||||||
|
|
||||||
|
window.Add(vbox)
|
||||||
|
window.SetSizeRequest(300, 400)
|
||||||
|
window.ShowAll()
|
||||||
|
|
||||||
|
dialog := gtk.NewDialog()
|
||||||
|
dialog.SetTitle(window.GetTitle())
|
||||||
|
sgroup := gtk.NewSizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
|
||||||
|
|
||||||
|
hbox := gtk.NewHBox(false, 1)
|
||||||
|
dialog.GetVBox().Add(hbox)
|
||||||
|
label := gtk.NewLabel("username:")
|
||||||
|
sgroup.AddWidget(label)
|
||||||
|
hbox.Add(label)
|
||||||
|
username := gtk.NewEntry()
|
||||||
|
hbox.Add(username)
|
||||||
|
|
||||||
|
hbox = gtk.NewHBox(false, 1)
|
||||||
|
dialog.GetVBox().Add(hbox)
|
||||||
|
label = gtk.NewLabel("password:")
|
||||||
|
sgroup.AddWidget(label)
|
||||||
|
hbox.Add(label)
|
||||||
|
password := gtk.NewEntry()
|
||||||
|
password.SetVisibility(false)
|
||||||
|
hbox.Add(password)
|
||||||
|
|
||||||
|
dialog.AddButton(gtk.STOCK_OK, gtk.RESPONSE_OK)
|
||||||
|
dialog.AddButton(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||||
|
dialog.SetDefaultResponse(gtk.RESPONSE_OK)
|
||||||
|
dialog.SetTransientFor(window)
|
||||||
|
dialog.ShowAll()
|
||||||
|
res := dialog.Run()
|
||||||
|
username_ := username.GetText()
|
||||||
|
password_ := password.GetText()
|
||||||
|
dialog.Destroy()
|
||||||
|
if res != gtk.RESPONSE_OK {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
xmpp.DefaultConfig = tls.Config{
|
||||||
|
ServerName: "talk.google.com",
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
talk, err := xmpp.NewClient("talk.google.com:443", username_, password_, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Connect("activate", func() {
|
||||||
|
text := entry.GetText()
|
||||||
|
tokens := strings.SplitN(text, " ", 2)
|
||||||
|
if len(tokens) == 2 {
|
||||||
|
func() {
|
||||||
|
defer recover()
|
||||||
|
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
|
||||||
|
entry.SetText("")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
func() {
|
||||||
|
defer recover()
|
||||||
|
chat, err := talk.Recv()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var iter gtk.TextIter
|
||||||
|
buffer.GetStartIter(&iter)
|
||||||
|
if msg, ok := chat.(xmpp.Chat); ok {
|
||||||
|
buffer.Insert(&iter, msg.Remote+": "+msg.Text+"\n")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
gtk.Main()
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/matterbridge/go-xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
server = flag.String("server", "talk.google.com:443", "server")
|
||||||
|
username = flag.String("username", "", "username")
|
||||||
|
password = flag.String("password", "", "password")
|
||||||
|
status = flag.String("status", "xa", "status")
|
||||||
|
statusMessage = flag.String("status-msg", "I for one welcome our new codebot overlords.", "status message")
|
||||||
|
notls = flag.Bool("notls", false, "No TLS")
|
||||||
|
debug = flag.Bool("debug", false, "debug output")
|
||||||
|
session = flag.Bool("session", false, "use server session")
|
||||||
|
)
|
||||||
|
|
||||||
|
func serverName(host string) string {
|
||||||
|
return strings.Split(host, ":")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: example [options]\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
if *username == "" || *password == "" {
|
||||||
|
if *debug && *username == "" && *password == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "no username or password were given; attempting ANONYMOUS auth\n")
|
||||||
|
} else if *username != "" || *password != "" {
|
||||||
|
flag.Usage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*notls {
|
||||||
|
xmpp.DefaultConfig = tls.Config{
|
||||||
|
ServerName: serverName(*server),
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var talk *xmpp.Client
|
||||||
|
var err error
|
||||||
|
options := xmpp.Options{
|
||||||
|
Host: *server,
|
||||||
|
User: *username,
|
||||||
|
Password: *password,
|
||||||
|
NoTLS: *notls,
|
||||||
|
Debug: *debug,
|
||||||
|
Session: *session,
|
||||||
|
Status: *status,
|
||||||
|
StatusMessage: *statusMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
talk, err = options.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
chat, err := talk.Recv()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
switch v := chat.(type) {
|
||||||
|
case xmpp.Chat:
|
||||||
|
fmt.Println(v.Remote, v.Text)
|
||||||
|
case xmpp.Presence:
|
||||||
|
fmt.Println(v.From, v.Show)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
in := bufio.NewReader(os.Stdin)
|
||||||
|
line, err := in.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = strings.TrimRight(line, "\n")
|
||||||
|
|
||||||
|
tokens := strings.SplitN(line, " ", 2)
|
||||||
|
if len(tokens) == 2 {
|
||||||
|
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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.0.0-20190611132908-4d4710463dbc
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
|
||||||
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
|
||||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gosrc.io/xmpp v0.0.0-20190611132908-4d4710463dbc h1:+XtYQ6hbNiPehZdPz3SU049S1wFFa4KKZxDtGITvyW8=
|
|
||||||
gosrc.io/xmpp v0.0.0-20190611132908-4d4710463dbc/go.mod h1:6NJG4vRCxQJMGLxIdroPLPd++FPLOmDqJdJEt2mu4kQ=
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# xmpp_component
|
|
||||||
|
|
||||||
This component will connect to ejabberd and act as a subdomain "service" of your primary XMPP domain
|
|
||||||
(in that case localhost).
|
|
||||||
|
|
||||||
To be able to connect this component, you need to add a listener to your XMPP server.
|
|
||||||
|
|
||||||
Here is an example ejabberd configuration for that component listener:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
listen:
|
|
||||||
...
|
|
||||||
-
|
|
||||||
port: 8888
|
|
||||||
module: ejabberd_service
|
|
||||||
password: "mypass"
|
|
||||||
```
|
|
||||||
|
|
||||||
ejabberd will listen for a component (service) on port 8888 and allows it to connect using the
|
|
||||||
secret "mypass".
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
opts := xmpp.ComponentOptions{
|
|
||||||
Domain: "service.localhost",
|
|
||||||
Secret: "mypass",
|
|
||||||
Address: "localhost:8888",
|
|
||||||
Name: "Test Component",
|
|
||||||
Category: "gateway",
|
|
||||||
Type: "service",
|
|
||||||
}
|
|
||||||
component, err := xmpp.NewComponent(opts)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If you pass the component to a connection manager, it will handle the reconnect policy
|
|
||||||
// for you automatically.
|
|
||||||
cm := xmpp.NewStreamManager(component, nil)
|
|
||||||
err = cm.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterator to receive packets coming from our XMPP connection
|
|
||||||
for packet := range component.Recv() {
|
|
||||||
switch p := packet.(type) {
|
|
||||||
case xmpp.IQ:
|
|
||||||
switch inner := p.Payload[0].(type) {
|
|
||||||
case *xmpp.DiscoInfo:
|
|
||||||
fmt.Println("DiscoInfo")
|
|
||||||
if p.Type == "get" {
|
|
||||||
discoResult(component, p.PacketAttrs, inner)
|
|
||||||
}
|
|
||||||
case *xmpp.DiscoItems:
|
|
||||||
fmt.Println("DiscoItems")
|
|
||||||
if p.Type == "get" {
|
|
||||||
discoItems(component, p.PacketAttrs, inner)
|
|
||||||
}
|
|
||||||
case *xmpp.Version:
|
|
||||||
fmt.Println("Version")
|
|
||||||
if p.Type == "get" {
|
|
||||||
version(component, p.PacketAttrs)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
fmt.Println("ignoring iq packet", inner)
|
|
||||||
xError := xmpp.Err{
|
|
||||||
Code: 501,
|
|
||||||
Reason: "feature-not-implemented",
|
|
||||||
Type: "cancel",
|
|
||||||
}
|
|
||||||
reply := p.MakeError(xError)
|
|
||||||
_ = component.Send(&reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
case xmpp.Message:
|
|
||||||
fmt.Println("Received message:", p.Body)
|
|
||||||
|
|
||||||
case xmpp.Presence:
|
|
||||||
fmt.Println("Received presence:", p.Type)
|
|
||||||
|
|
||||||
default:
|
|
||||||
fmt.Println("ignoring packet:", packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func discoResult(c *xmpp.Component, attrs xmpp.PacketAttrs, info *xmpp.DiscoInfo) {
|
|
||||||
iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
|
|
||||||
var identity xmpp.Identity
|
|
||||||
if info.Node == "" {
|
|
||||||
identity = xmpp.Identity{
|
|
||||||
Name: c.Name,
|
|
||||||
Category: c.Category,
|
|
||||||
Type: c.Type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := xmpp.DiscoInfo{
|
|
||||||
Identity: identity,
|
|
||||||
Features: []xmpp.Feature{
|
|
||||||
{Var: xmpp.NSDiscoInfo},
|
|
||||||
{Var: xmpp.NSDiscoItems},
|
|
||||||
{Var: "jabber:iq:version"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
iq.AddPayload(&payload)
|
|
||||||
|
|
||||||
_ = c.Send(iq)
|
|
||||||
}
|
|
||||||
|
|
||||||
func discoItems(c *xmpp.Component, attrs xmpp.PacketAttrs, items *xmpp.DiscoItems) {
|
|
||||||
iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
|
|
||||||
|
|
||||||
var payload xmpp.DiscoItems
|
|
||||||
if items.Node == "" {
|
|
||||||
payload = xmpp.DiscoItems{
|
|
||||||
Items: []xmpp.DiscoItem{
|
|
||||||
{Name: "test node", JID: "service.localhost", Node: "node1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iq.AddPayload(&payload)
|
|
||||||
_ = c.Send(iq)
|
|
||||||
}
|
|
||||||
|
|
||||||
func version(c *xmpp.Component, attrs xmpp.PacketAttrs) {
|
|
||||||
iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
|
|
||||||
|
|
||||||
var payload xmpp.Version
|
|
||||||
payload.Name = "Fluux XMPP Component"
|
|
||||||
payload.Version = "0.0.1"
|
|
||||||
iq.AddPayload(&payload)
|
|
||||||
_ = c.Send(iq)
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
xmpp_echo is a demo client that connect on an XMPP server and echo message received back to original sender.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
config := xmpp.Config{
|
|
||||||
Address: "localhost:5222",
|
|
||||||
Jid: "test@localhost",
|
|
||||||
Password: "test",
|
|
||||||
PacketLogger: os.Stdout,
|
|
||||||
Insecure: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config)
|
|
||||||
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)
|
|
||||||
err = cm.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterator to receive packets coming from our XMPP connection
|
|
||||||
for packet := range client.Recv() {
|
|
||||||
switch packet := packet.(type) {
|
|
||||||
case xmpp.Message:
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
|
|
||||||
reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body}
|
|
||||||
_ = client.Send(reply)
|
|
||||||
default:
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
|
|
||||||
// (using templates ?)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// Can be launched with:
|
|
||||||
// ./xmpp_jukebox -jid=test@localhost/jukebox -password=test -address=localhost:5222
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/processone/mpg123"
|
|
||||||
"github.com/processone/soundcloud"
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
var client *xmpp.Client
|
|
||||||
var err error
|
|
||||||
if client, err = connectXmpp(*jid, *password, *address); err != nil {
|
|
||||||
log.Fatal("Could not connect to XMPP: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := mpg123.NewPlayer()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterator to receive packets coming from our XMPP connection
|
|
||||||
for packet := range client.Recv() {
|
|
||||||
|
|
||||||
switch packet := packet.(type) {
|
|
||||||
case xmpp.Message:
|
|
||||||
processMessage(client, p, &packet)
|
|
||||||
case xmpp.IQ:
|
|
||||||
processIq(client, p, &packet)
|
|
||||||
case xmpp.Presence:
|
|
||||||
// Do nothing with received presence
|
|
||||||
default:
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.Message) {
|
|
||||||
command := strings.Trim(packet.Body, " ")
|
|
||||||
if command == "stop" {
|
|
||||||
p.Stop()
|
|
||||||
} else {
|
|
||||||
playSCURL(p, command)
|
|
||||||
sendUserTune(client, "Radiohead", "Spectre")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) {
|
|
||||||
switch payload := packet.Payload[0].(type) {
|
|
||||||
// We support IOT Control IQ
|
|
||||||
case *xmpp.ControlSet:
|
|
||||||
var url string
|
|
||||||
for _, element := range payload.Fields {
|
|
||||||
if element.XMLName.Local == "string" && element.Name == "url" {
|
|
||||||
url = strings.Trim(element.Value, " ")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playSCURL(p, url)
|
|
||||||
setResponse := new(xmpp.ControlSetResponse)
|
|
||||||
reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: []xmpp.IQPayload{setResponse}}
|
|
||||||
_ = client.Send(reply)
|
|
||||||
// TODO add Soundclound artist / title retrieval
|
|
||||||
sendUserTune(client, "Radiohead", "Spectre")
|
|
||||||
default:
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", packet.Payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendUserTune(client *xmpp.Client, artist string, title string) {
|
|
||||||
tune := xmpp.Tune{Artist: artist, Title: title}
|
|
||||||
iq := xmpp.NewIQ("set", "", "", "usertune-1", "en")
|
|
||||||
payload := xmpp.PubSub{Publish: xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: tune}}}
|
|
||||||
iq.AddPayload(&payload)
|
|
||||||
_ = client.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectXmpp(jid string, password string, address string) (client *xmpp.Client, err error) {
|
|
||||||
xmppConfig := xmpp.Config{Address: address,
|
|
||||||
Jid: jid, Password: password, PacketLogger: os.Stdout, Insecure: true}
|
|
||||||
|
|
||||||
if client, err = xmpp.NewClient(xmppConfig); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Connect(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// - Have a player API to play, play next, or add to queue
|
|
||||||
// - Have the ability to parse custom packet to play sound
|
|
||||||
// - Use PEP to display tunes status
|
|
||||||
// - Ability to "speak" messages
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f 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>", nsSASL, enc)
|
|
||||||
|
|
||||||
// Next message should be either success or failure.
|
|
||||||
val, err := next(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := val.(type) {
|
|
||||||
case SASLSuccess:
|
|
||||||
case 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SASLSuccess
|
|
||||||
|
|
||||||
type SASLSuccess struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (SASLSuccess) Name() string {
|
|
||||||
return "sasl:success"
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type auth struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
|
||||||
Mechanism string `xml:"mecanism,attr"`
|
|
||||||
Value string `xml:",innerxml"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BindBind struct {
|
|
||||||
IQPayload
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
|
||||||
Resource string `xml:"resource,omitempty"`
|
|
||||||
Jid string `xml:"jid,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session is obsolete in RFC 6121.
|
|
||||||
// Added for compliance with RFC 3121.
|
|
||||||
// Remove when ejabberd purely conforms to RFC 6121.
|
|
||||||
type sessionSession struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
|
||||||
optional xml.Name // If it does exist, it mean we are not required to open session
|
|
||||||
}
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
Interesting reference on backoff:
|
|
||||||
- Exponential Backoff And Jitter (AWS Blog):
|
|
||||||
https://www.awsarchitectureblog.com/2015/03/backoff.html
|
|
||||||
|
|
||||||
We use Jitter as a default for exponential backoff, as the goal of
|
|
||||||
this module is not to provide precise 'ticks', but good behaviour to
|
|
||||||
implement retries that are helping the server to recover faster in
|
|
||||||
case of congestion.
|
|
||||||
|
|
||||||
It can be used in several ways:
|
|
||||||
- Using duration to get next sleep time.
|
|
||||||
- Using ticker channel to trigger callback function on tick
|
|
||||||
|
|
||||||
The functions for Backoff are not threadsafe, but you can:
|
|
||||||
- Keep the attempt counter on your end and use DurationForAttempt(int)
|
|
||||||
- Use lock in your own code to protect the Backoff structure.
|
|
||||||
|
|
||||||
TODO: Implement Backoff Ticker channel
|
|
||||||
TODO: Implement throttler interface. Throttler could be used to implement various reconnect strategies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultBase int = 20 // Backoff base, in ms
|
|
||||||
defaultFactor int = 2
|
|
||||||
defaultCap int = 180000 // 3 minutes
|
|
||||||
)
|
|
||||||
|
|
||||||
// Backoff can provide 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 successfull operation has been performed,
|
|
||||||
// to reset the exponential backoff interval.
|
|
||||||
func (b *Backoff) Reset() {
|
|
||||||
b.attempt = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Backoff) setDefault() {
|
|
||||||
if b.Base == 0 {
|
|
||||||
b.Base = defaultBase
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Cap == 0 {
|
|
||||||
b.Cap = defaultCap
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Factor == 0 {
|
|
||||||
b.Factor = defaultFactor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
We use full jitter as default for now as it seems to provide good behaviour for reconnect.
|
|
||||||
|
|
||||||
Base is the default interval between attempts (if backoff Factor was equal to 1)
|
|
||||||
|
|
||||||
Attempt is the number of retry for operation. If we start attempt at 0, first sleep equals base.
|
|
||||||
|
|
||||||
Cap is the maximum sleep time duration we tolerate between attempts
|
|
||||||
*/
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package xmpp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDurationForAttempt_NoJitter(t *testing.T) {
|
|
||||||
b := xmpp.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-145
@@ -1,145 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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, NSClient, NSStream); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set xml decoder and extract streamID from reply (not used for now)
|
|
||||||
_, err = initDecoder(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract stream features
|
|
||||||
var f StreamFeatures
|
|
||||||
packet, err := next(decoder)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("stream open decode features: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p := packet.(type) {
|
|
||||||
case StreamFeatures:
|
|
||||||
f = p
|
|
||||||
case 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 tlsProceed
|
|
||||||
if err = decoder.DecodeElement(&k, nil); err != nil {
|
|
||||||
return fmt.Errorf("expecting starttls proceed: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultTlsConfig.ServerName = c.domain
|
|
||||||
tlsConn := tls.Client(tcpconn, &DefaultTlsConfig)
|
|
||||||
// We convert existing connection to TLS
|
|
||||||
if err = tlsConn.Handshake(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We check that cert matches hostname
|
|
||||||
if err = tlsConn.VerifyHostname(c.domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = checkExpiration(tlsConn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New("TLS not supported on server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check expiration date for the whole certificate chain and returns an error
|
|
||||||
// if the expiration date is in less than 48 hours.
|
|
||||||
func checkExpiration(tlsConn *tls.Conn) error {
|
|
||||||
checkedCerts := make(map[string]struct{})
|
|
||||||
for _, chain := range tlsConn.ConnectionState().VerifiedChains {
|
|
||||||
for _, cert := range chain {
|
|
||||||
if _, checked := checkedCerts[string(cert.Signature)]; checked {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
checkedCerts[string(cert.Signature)] = struct{}{}
|
|
||||||
|
|
||||||
// Check the expiration.
|
|
||||||
timeNow := time.Now()
|
|
||||||
expiresInHours := int64(cert.NotAfter.Sub(timeNow).Hours())
|
|
||||||
// fmt.Printf("Cert '%s' expires in %d days\n", cert.Subject.CommonName, expiresInHours/24)
|
|
||||||
if expiresInHours <= 48 {
|
|
||||||
return fmt.Errorf("certificate '%s' will expire on %s", cert.Subject.CommonName, cert.NotAfter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractParams(addr string) (string, string, error) {
|
|
||||||
var err error
|
|
||||||
hostport := strings.Split(addr, ":")
|
|
||||||
if len(hostport) > 2 {
|
|
||||||
err = errors.New("too many colons in xmpp server address")
|
|
||||||
return addr, hostport[0], err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address is composed of two parts, we are good
|
|
||||||
if len(hostport) == 2 && hostport[1] != "" {
|
|
||||||
return addr, hostport[0], err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Port was not passed, we append XMPP default port:
|
|
||||||
return strings.Join([]string{hostport[0], "5222"}, ":"), hostport[0], err
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// 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
|
|
||||||
// Packet channel
|
|
||||||
RecvChannel chan Packet
|
|
||||||
// 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.
|
|
||||||
// TODO: better config checks
|
|
||||||
func NewClient(config Config) (c *Client, err error) {
|
|
||||||
// TODO: If option address is nil, use the Jid domain to compose the address
|
|
||||||
if config.Address, err = checkAddress(config.Address); err != nil {
|
|
||||||
return nil, NewConnError(err, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Password == "" {
|
|
||||||
err = errors.New("missing password")
|
|
||||||
return nil, NewConnError(err, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JID
|
|
||||||
if config.parsedJid, err = NewJid(config.Jid); err != nil {
|
|
||||||
err = errors.New("missing jid")
|
|
||||||
return nil, NewConnError(err, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
c = new(Client)
|
|
||||||
c.config = config
|
|
||||||
|
|
||||||
if c.config.ConnectTimeout == 0 {
|
|
||||||
c.config.ConnectTimeout = 15 // 15 second as default
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a default channel that developers can override
|
|
||||||
c.RecvChannel = make(chan Packet)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Pass JID to be able to add default address based on JID, if addr is empty
|
|
||||||
func checkAddress(addr 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, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address is composed of two parts, we are good
|
|
||||||
if len(hostport) == 2 && hostport[1] != "" {
|
|
||||||
return addr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Port was not passed, we append XMPP default port:
|
|
||||||
return strings.Join([]string{hostport[0], "5222"}, ":"), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.socketProxy, "<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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recv abstracts receiving preparsed XMPP packets from a channel.
|
|
||||||
// Channel allow client to receive / dispatch packets in for range loop.
|
|
||||||
// TODO: Deprecate this function in favor of reading directly from the RecvChannel ?
|
|
||||||
func (c *Client) Recv() <-chan Packet {
|
|
||||||
return c.RecvChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send marshals XMPP stanza and sends it to the server.
|
|
||||||
func (c *Client) Send(packet Packet) error {
|
|
||||||
data, err := xml.Marshal(packet)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("cannot marshal packet " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := fmt.Fprintf(c.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 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 {
|
|
||||||
var err error
|
|
||||||
_, err = fmt.Fprintf(c.Session.socketProxy, packet)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Go routines
|
|
||||||
|
|
||||||
// Loop: Receive data from server
|
|
||||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
|
||||||
for {
|
|
||||||
val, err := next(c.Session.decoder)
|
|
||||||
if err != nil {
|
|
||||||
close(keepaliveQuit)
|
|
||||||
c.updateState(StateDisconnected)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle stream errors
|
|
||||||
switch packet := val.(type) {
|
|
||||||
case StreamError:
|
|
||||||
c.RecvChannel <- val
|
|
||||||
close(c.RecvChannel)
|
|
||||||
c.streamError(packet.Error.Local, packet.Text)
|
|
||||||
return errors.New("stream error: " + packet.Error.Local)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.RecvChannel <- 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-207
@@ -1,207 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
if client, err = NewClient(config); 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
|
|
||||||
if client, err = NewClient(config); 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
|
|
||||||
if client, err = NewClient(config); 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()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// 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'>"
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 != 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", NSClient, 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 := nextStart(decoder)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read auth: %s", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var nv interface{}
|
|
||||||
nv = &auth{}
|
|
||||||
// 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 *auth:
|
|
||||||
return v.Value
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
|
||||||
// This is a basic server, supporting only 1 stream feature: SASL Plain Auth
|
|
||||||
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 bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
|
||||||
se, err := nextStart(decoder)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read bind: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iq := &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[0].(type) {
|
|
||||||
case *BindBind:
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# XMPP Check
|
|
||||||
|
|
||||||
XMPP check is a tool to check TLS certificate on a remote server.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
$ 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.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
- Use a config file to define the checks to perform as client on an XMPP server.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
comment: off
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
build:
|
|
||||||
build:
|
|
||||||
image: fluux/build
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
encrypted_env_file: codeship.env.encrypted
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
- type: serial
|
|
||||||
steps:
|
|
||||||
- name: test
|
|
||||||
service: build
|
|
||||||
command: ./test.sh
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
yVKgVFeKW6SSnC/KgLYpfYtTcqqTke1gOIW5GUiVvRijnhweOJiYKFPmwPjpt1FVrg4WVELQUNbxn3lmfyHVVF7r
|
|
||||||
-220
@@ -1,220 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// Packet channel
|
|
||||||
RecvChannel chan Packet
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// TCP level connection
|
|
||||||
conn net.Conn
|
|
||||||
|
|
||||||
// read / write
|
|
||||||
socketProxy io.ReadWriter // TODO
|
|
||||||
decoder *xml.Decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewComponent(opts ComponentOptions) (*Component, error) {
|
|
||||||
c := Component{ComponentOptions: opts}
|
|
||||||
// Create a default channel that developers can override
|
|
||||||
c.RecvChannel = make(chan Packet)
|
|
||||||
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, NSComponent, 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 := initDecoder(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 := next(c.decoder)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := val.(type) {
|
|
||||||
case StreamError:
|
|
||||||
return errors.New("handshake failed " + v.Error.Local)
|
|
||||||
case 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recv abstracts receiving preparsed XMPP packets from a channel.
|
|
||||||
// Channel allow client to receive / dispatch packets in for range loop.
|
|
||||||
// TODO: Deprecate this function in favor of reading directly from the RecvChannel ?
|
|
||||||
func (c *Component) Recv() <-chan Packet {
|
|
||||||
return c.RecvChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Component) recv() (err error) {
|
|
||||||
for {
|
|
||||||
val, err := next(c.decoder)
|
|
||||||
if err != nil {
|
|
||||||
c.updateState(StateDisconnected)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle stream errors
|
|
||||||
switch p := val.(type) {
|
|
||||||
case StreamError:
|
|
||||||
c.RecvChannel <- val
|
|
||||||
close(c.RecvChannel)
|
|
||||||
c.streamError(p.Error.Local, p.Text)
|
|
||||||
return errors.New("stream error: " + p.Error.Local)
|
|
||||||
}
|
|
||||||
c.RecvChannel <- val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send marshalls XMPP stanza and sends it to the server.
|
|
||||||
func (c *Component) Send(packet Packet) error {
|
|
||||||
data, err := xml.Marshal(packet)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("cannot marshal packet " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := fmt.Fprintf(c.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 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: Add support for discovery management directly in component
|
|
||||||
TODO: Support multiple identities on disco info
|
|
||||||
TODO: Support returning features on disco info
|
|
||||||
*/
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestHandshake(t *testing.T) {
|
|
||||||
opts := ComponentOptions{
|
|
||||||
Domain: "test.localhost",
|
|
||||||
Secret: "mypass",
|
|
||||||
}
|
|
||||||
c := Component{ComponentOptions: opts}
|
|
||||||
|
|
||||||
streamID := "1263952298440005243"
|
|
||||||
expected := "c77e2ef0109fbbc5161e83b51629cd1353495332"
|
|
||||||
|
|
||||||
result := c.handshake(streamID)
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("incorrect handshake calculation '%s' != '%s'", result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateHandshake(t *testing.T) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Address string
|
|
||||||
Jid string
|
|
||||||
parsedJid *Jid // For easier manipulation
|
|
||||||
Password string
|
|
||||||
PacketLogger *os.File // Used for debugging
|
|
||||||
Lang string // TODO: should default to 'en'
|
|
||||||
ConnectTimeout int // Client timeout in seconds. Default to 15
|
|
||||||
// Insecure can be set to true to allow to open a session without TLS. If TLS
|
|
||||||
// is supported on the server, we will still try to use it.
|
|
||||||
Insecure bool
|
|
||||||
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConnError struct {
|
|
||||||
frame xerrors.Frame
|
|
||||||
err error
|
|
||||||
// Permanent will be true if error is not recoverable
|
|
||||||
Permanent bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConnError(err error, permanent bool) ConnError {
|
|
||||||
return ConnError{err: err, frame: xerrors.Caller(1), Permanent: permanent}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ConnError) Format(s fmt.State, verb rune) {
|
|
||||||
xerrors.FormatError(e, s, verb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ConnError) FormatError(p xerrors.Printer) error {
|
|
||||||
e.frame.Format(p)
|
|
||||||
return e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ConnError) Error() string {
|
|
||||||
return fmt.Sprint(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ConnError) Unwrap() error { return e.err }
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
|
||||||
|
|
||||||
The goal is to make simple to write simple adhoc XMPP clients:
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
Fluux XMPP can be used to build XMPP clients or XMPP components.
|
|
||||||
|
|
||||||
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 // import "gosrc.io/xmpp"
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
module gosrc.io/xmpp
|
module github.com/matterbridge/go-xmpp
|
||||||
|
|
||||||
go 1.9
|
go 1.21.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/go-cmp v0.2.0
|
golang.org/x/crypto v0.23.0
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
golang.org/x/net v0.25.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ControlSet struct {
|
|
||||||
IQPayload
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
|
|
||||||
Fields []ControlField `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
IQPayload
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
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[0].(*ControlSet); !ok {
|
|
||||||
t.Errorf("Paylod is not an iot control set: %v", cs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO support ability to put Raw payload inside IQ
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 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 {
|
|
||||||
IQPayload
|
|
||||||
XMLName xml.Name `xml:"error"`
|
|
||||||
Code int `xml:"code,attr,omitempty"`
|
|
||||||
Type string `xml:"type,attr,omitempty"`
|
|
||||||
Reason string
|
|
||||||
Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing for IQs
|
|
||||||
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 = 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: 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})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// IQ Packet
|
|
||||||
|
|
||||||
type IQ struct { // Info/Query
|
|
||||||
XMLName xml.Name `xml:"iq"`
|
|
||||||
PacketAttrs
|
|
||||||
Payload []IQPayload `xml:",omitempty"`
|
|
||||||
RawXML string `xml:",innerxml"`
|
|
||||||
Error Err `xml:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIQ(iqtype, from, to, id, lang string) IQ {
|
|
||||||
return IQ{
|
|
||||||
XMLName: xml.Name{Local: "iq"},
|
|
||||||
PacketAttrs: PacketAttrs{
|
|
||||||
Id: id,
|
|
||||||
From: from,
|
|
||||||
To: to,
|
|
||||||
Type: iqtype,
|
|
||||||
Lang: lang,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (iq *IQ) AddPayload(payload IQPayload) {
|
|
||||||
iq.Payload = append(iq.Payload, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "to" {
|
|
||||||
iq.To = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "from" {
|
|
||||||
iq.From = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "lang" {
|
|
||||||
iq.Lang = attr.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode inner elements
|
|
||||||
level := 0
|
|
||||||
for {
|
|
||||||
t, err := d.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tt := t.(type) {
|
|
||||||
|
|
||||||
case xml.StartElement:
|
|
||||||
level++
|
|
||||||
if level <= 1 {
|
|
||||||
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
|
||||||
// Decode payload extension
|
|
||||||
err = d.DecodeElement(iqExt, &tt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
iq.Payload = append(iq.Payload, iqExt)
|
|
||||||
} else {
|
|
||||||
// TODO: Fix me. We do nothing of that element here.
|
|
||||||
// elt = new(Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case xml.EndElement:
|
|
||||||
level--
|
|
||||||
if tt == start.End() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Generic IQ Payload
|
|
||||||
|
|
||||||
type IQPayload interface{}
|
|
||||||
|
|
||||||
// Node is a generic structure to represent XML data. It is used to parse
|
|
||||||
// unreferenced or custom stanza payload.
|
|
||||||
type Node struct {
|
|
||||||
IQPayload
|
|
||||||
XMLName xml.Name
|
|
||||||
Attrs []xml.Attr `xml:"-"`
|
|
||||||
Content string `xml:",innerxml"`
|
|
||||||
Nodes []Node `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Disco
|
|
||||||
|
|
||||||
const (
|
|
||||||
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
|
||||||
NSDiscoItems = "http://jabber.org/protocol/disco#items"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Disco Info
|
|
||||||
type DiscoInfo struct {
|
|
||||||
IQPayload
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Items
|
|
||||||
type DiscoItems struct {
|
|
||||||
IQPayload
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
|
||||||
Node string `xml:"node,attr,omitempty"`
|
|
||||||
Items []DiscoItem `xml:"item"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Software Version (XEP-0092)
|
|
||||||
|
|
||||||
// Version
|
|
||||||
type Version struct {
|
|
||||||
IQPayload
|
|
||||||
XMLName xml.Name `xml:"jabber:iq:version query"`
|
|
||||||
Name string `xml:"name,omitempty"`
|
|
||||||
Version string `xml:"version,omitempty"`
|
|
||||||
OS string `xml:"os,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Registry init
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{})
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
|
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
|
|
||||||
}
|
|
||||||
-129
@@ -1,129 +0,0 @@
|
|||||||
package xmpp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnmarshalIqs(t *testing.T) {
|
|
||||||
//var cs1 = new(iot.ControlSet)
|
|
||||||
var tests = []struct {
|
|
||||||
iqString string
|
|
||||||
parsedIQ xmpp.IQ
|
|
||||||
}{
|
|
||||||
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
|
|
||||||
xmpp.IQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: xmpp.PacketAttrs{To: "test@localhost", Type: "set", 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 := xmpp.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 := xmpp.NewIQ("result", "admin@localhost", "test@localhost", "1", "en")
|
|
||||||
payload := xmpp.DiscoInfo{
|
|
||||||
Identity: xmpp.Identity{
|
|
||||||
Name: "Test Gateway",
|
|
||||||
Category: "gateway",
|
|
||||||
Type: "mqtt",
|
|
||||||
},
|
|
||||||
Features: []xmpp.Feature{
|
|
||||||
{Var: xmpp.NSDiscoInfo},
|
|
||||||
{Var: xmpp.NSDiscoItems},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
iq.AddPayload(&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 := xmpp.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 TestErrorTag(t *testing.T) {
|
|
||||||
xError := xmpp.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 := xmpp.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 := xmpp.NewIQ("get", "romeo@montague.net/orchard", "catalog.shakespeare.lit", "items3", "en")
|
|
||||||
payload := xmpp.DiscoItems{
|
|
||||||
Node: "music",
|
|
||||||
}
|
|
||||||
iq.AddPayload(&payload)
|
|
||||||
|
|
||||||
data, err := xml.Marshal(iq)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot marshal xml structure")
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedIQ := xmpp.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 := xmpp.IQ{}
|
|
||||||
err := xml.Unmarshal([]byte(query), &parsedIQ)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error", query)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parsedIQ.Payload) != 1 {
|
|
||||||
t.Errorf("Incorrect payload size: %d", len(parsedIQ.Payload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Jid struct {
|
|
||||||
Node string
|
|
||||||
Domain string
|
|
||||||
Resource string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJid(sjid string) (*Jid, error) {
|
|
||||||
jid := new(Jid)
|
|
||||||
|
|
||||||
if sjid == "" {
|
|
||||||
return jid, fmt.Errorf("jid cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
s1 := strings.SplitN(sjid, "@", 2)
|
|
||||||
if len(s1) == 1 { // This is a server or component JID
|
|
||||||
jid.Domain = s1[0]
|
|
||||||
} else { // JID has a local username part
|
|
||||||
if s1[0] == "" {
|
|
||||||
return jid, fmt.Errorf("invalid jid '%s", sjid)
|
|
||||||
}
|
|
||||||
jid.Node = s1[0]
|
|
||||||
if s1[1] == "" {
|
|
||||||
return jid, fmt.Errorf("domain cannot be empty")
|
|
||||||
}
|
|
||||||
jid.Domain = s1[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract resource from domain field
|
|
||||||
s2 := strings.SplitN(jid.Domain, "/", 2)
|
|
||||||
if len(s2) == 2 { // If len = 1, domain is already correct, and resource is already empty
|
|
||||||
jid.Domain = s2[0]
|
|
||||||
jid.Resource = s2[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isUsernameValid(jid.Node) {
|
|
||||||
return jid, fmt.Errorf("invalid Node in JID '%s'", sjid)
|
|
||||||
}
|
|
||||||
if !isDomainValid(jid.Domain) {
|
|
||||||
return jid, fmt.Errorf("invalid domain in JID '%s'", sjid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return jid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jid) Full() string {
|
|
||||||
return j.Node + "@" + j.Domain + "/" + j.Resource
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jid) Bare() string {
|
|
||||||
return j.Node + "@" + j.Domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helpers, for parsing / validation
|
|
||||||
|
|
||||||
func isUsernameValid(username string) bool {
|
|
||||||
invalidRunes := []rune{'@', '/', '\'', '"', ':', '<', '>'}
|
|
||||||
return strings.IndexFunc(username, isInvalid(invalidRunes)) < 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDomainValid(domain string) bool {
|
|
||||||
if len(domain) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidRunes := []rune{'@', '/'}
|
|
||||||
return strings.IndexFunc(domain, isInvalid(invalidRunes)) < 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func isInvalid(invalidRunes []rune) func(c rune) bool {
|
|
||||||
isInvalid := func(c rune) bool {
|
|
||||||
if unicode.IsSpace(c) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, r := range invalidRunes {
|
|
||||||
if c == r {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isInvalid
|
|
||||||
}
|
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-120
@@ -1,120 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Message Packet
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
XMLName xml.Name `xml:"message"`
|
|
||||||
PacketAttrs
|
|
||||||
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(msgtype, from, to, id, lang string) Message {
|
|
||||||
return Message{
|
|
||||||
XMLName: xml.Name{Local: "message"},
|
|
||||||
PacketAttrs: PacketAttrs{
|
|
||||||
Id: id,
|
|
||||||
From: from,
|
|
||||||
To: to,
|
|
||||||
Type: msgtype,
|
|
||||||
Lang: lang,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 IQs
|
|
||||||
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 = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "to" {
|
|
||||||
msg.To = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "from" {
|
|
||||||
msg.From = attr.Value
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "lang" {
|
|
||||||
msg.Lang = attr.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode inner elements
|
|
||||||
for {
|
|
||||||
t, err := d.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tt := t.(type) {
|
|
||||||
|
|
||||||
case xml.StartElement:
|
|
||||||
if msgExt := TypeRegistry.GetMsgExtension(tt.Name); msgExt != nil {
|
|
||||||
// Decode message extension
|
|
||||||
err = d.DecodeElement(msgExt, &tt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
msg.Extensions = append(msg.Extensions, msgExt)
|
|
||||||
} else {
|
|
||||||
// Decode standard message sub-elements
|
|
||||||
var err error
|
|
||||||
switch tt.Name.Local {
|
|
||||||
case "body":
|
|
||||||
err = d.DecodeElement(&msg.Body, &tt)
|
|
||||||
case "thread":
|
|
||||||
err = d.DecodeElement(&msg.Thread, &tt)
|
|
||||||
case "subject":
|
|
||||||
err = d.DecodeElement(&msg.Subject, &tt)
|
|
||||||
case "error":
|
|
||||||
err = d.DecodeElement(&msg.Error, &tt)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case xml.EndElement:
|
|
||||||
if tt == start.End() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package xmpp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGenerateMessage(t *testing.T) {
|
|
||||||
message := xmpp.NewMessage("chat", "admin@localhost", "test@localhost", "1", "en")
|
|
||||||
message.Body = "Hi"
|
|
||||||
message.Subject = "Msg Subject"
|
|
||||||
|
|
||||||
data, err := xml.Marshal(message)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot marshal xml structure")
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedMessage := xmpp.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 := xmpp.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import "encoding/xml"
|
|
||||||
|
|
||||||
/*
|
|
||||||
Support for:
|
|
||||||
- XEP-0333 - Chat Markers: https://xmpp.org/extensions/xep-0333.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NSMsgChatMarkers = "urn:xmpp:chat-markers:0"
|
|
||||||
|
|
||||||
type Markable struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 markable"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarkReceived struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 received"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarkDisplayed struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 displayed"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarkAcknowledged struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 acknowledged"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "markable"}, Markable{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "received"}, MarkReceived{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "displayed"}, MarkDisplayed{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "acknowledged"}, MarkAcknowledged{})
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
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{})
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import "encoding/xml"
|
|
||||||
|
|
||||||
/*
|
|
||||||
Support for:
|
|
||||||
- XEP-0066 - Out of Band Data: https://xmpp.org/extensions/xep-0066.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
type OOB struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"jabber:x:oob x"`
|
|
||||||
URL string `xml:"url"`
|
|
||||||
Desc string `xml:"desc,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"jabber:x:oob", "x"}, OOB{})
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import "encoding/xml"
|
|
||||||
|
|
||||||
/*
|
|
||||||
Support for:
|
|
||||||
- XEP-0184 - Message Delivery Receipts: https://xmpp.org/extensions/xep-0184.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NSMsgReceipts = "urn:xmpp:receipts"
|
|
||||||
|
|
||||||
// Used on outgoing message, to tell the recipient that you are requesting a message receipt / ack.
|
|
||||||
type ReceiptRequest struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:receipts request"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReceiptReceived struct {
|
|
||||||
MsgExtension
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:receipts received"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "request"}, ReceiptRequest{})
|
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "received"}, ReceiptReceived{})
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package xmpp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 := xmpp.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 *xmpp.ReceiptRequest:
|
|
||||||
if ext.XMLName.Local != "request" {
|
|
||||||
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Errorf("could not find receipts extension")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
const (
|
|
||||||
NSStream = "http://etherx.jabber.org/streams"
|
|
||||||
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
|
||||||
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
|
||||||
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
|
||||||
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
|
||||||
NSClient = "jabber:client"
|
|
||||||
NSComponent = "jabber:component:accept"
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
type Packet interface {
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// PacketAttrs represents the common structure for base XMPP packets.
|
|
||||||
type PacketAttrs struct {
|
|
||||||
Id string `xml:"id,attr,omitempty"`
|
|
||||||
From string `xml:"from,attr,omitempty"`
|
|
||||||
To string `xml:"to,attr,omitempty"`
|
|
||||||
Type string `xml:"type,attr,omitempty"`
|
|
||||||
Lang string `xml:"lang,attr,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type packetFormatter interface {
|
|
||||||
XMPPFormat() string
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
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 initDecoder(p *xml.Decoder) (sessionID string, err error) {
|
|
||||||
for {
|
|
||||||
var t xml.Token
|
|
||||||
t, err = p.Token()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Stream attributes
|
|
||||||
for _, attrs := range elem.Attr {
|
|
||||||
switch attrs.Name.Local {
|
|
||||||
case "id":
|
|
||||||
sessionID = attrs.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// next scans XML token stream for next element and then assign a structure to decode
|
|
||||||
// that elements.
|
|
||||||
// TODO Use an interface to return packets interface xmppDecoder
|
|
||||||
func next(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 + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
|
||||||
switch se.Name.Local {
|
|
||||||
case "handshake":
|
|
||||||
return handshake.decode(p, se)
|
|
||||||
case "message":
|
|
||||||
return message.decode(p, se)
|
|
||||||
case "presence":
|
|
||||||
return presence.decode(p, se)
|
|
||||||
case "iq":
|
|
||||||
return iq.decode(p, se)
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unexpected XMPP packet " +
|
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PubSub struct {
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
|
|
||||||
Publish Publish
|
|
||||||
}
|
|
||||||
|
|
||||||
type Publish struct {
|
|
||||||
XMLName xml.Name `xml:"publish"`
|
|
||||||
Node string `xml:"node,attr"`
|
|
||||||
Item Item
|
|
||||||
}
|
|
||||||
|
|
||||||
type Item struct {
|
|
||||||
XMLName xml.Name `xml:"item"`
|
|
||||||
Tune Tune
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
type PubsubPublish struct {
|
|
||||||
XMLName xml.Name `xml:"publish"`
|
|
||||||
node string `xml:"node,attr"`
|
|
||||||
item PubSubItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type PubSubItem struct {
|
|
||||||
xmlName xml.Name `xml:"item"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Thing2 struct {
|
|
||||||
XMLName xml.Name `xml:"publish"`
|
|
||||||
node string `xml:"node,attr"`
|
|
||||||
tune string `xml:"http://jabber.org/protocol/tune item>tune"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tune struct {
|
|
||||||
artist string
|
|
||||||
length int
|
|
||||||
rating int
|
|
||||||
source string
|
|
||||||
title string
|
|
||||||
track string
|
|
||||||
uri string
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
func (*Tune) XMPPFormat() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<iq type='set' id='%s'>
|
|
||||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
|
||||||
<publish node='http://jabber.org/protocol/tune'>
|
|
||||||
<item>
|
|
||||||
<tune xmlns='http://jabber.org/protocol/tune'>
|
|
||||||
<artist>%s</artist>
|
|
||||||
<length>%i</length>
|
|
||||||
<rating>%i</rating>
|
|
||||||
<source>%s</source>
|
|
||||||
<title>%s</title>
|
|
||||||
<track>%s</track>
|
|
||||||
<uri>%s</uri>
|
|
||||||
</tune>
|
|
||||||
</item>
|
|
||||||
</publish>
|
|
||||||
</pubsub>
|
|
||||||
</iq>`)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import "encoding/xml"
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Presence Packet
|
|
||||||
|
|
||||||
type Presence struct {
|
|
||||||
XMLName xml.Name `xml:"presence"`
|
|
||||||
PacketAttrs
|
|
||||||
Show string `xml:"show,omitempty"` // away, chat, dnd, xa
|
|
||||||
Status string `xml:"status,omitempty"`
|
|
||||||
Priority string `xml:"priority,omitempty"`
|
|
||||||
Error Err `xml:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Presence) Name() string {
|
|
||||||
return "presence"
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPresence(from, to, id, lang string) Presence {
|
|
||||||
return Presence{
|
|
||||||
XMLName: xml.Name{Local: "presence"},
|
|
||||||
PacketAttrs: PacketAttrs{
|
|
||||||
Id: id,
|
|
||||||
From: from,
|
|
||||||
To: to,
|
|
||||||
Lang: lang,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package xmpp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGeneratePresence(t *testing.T) {
|
|
||||||
presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en")
|
|
||||||
presence.Show = "chat"
|
|
||||||
|
|
||||||
data, err := xml.Marshal(presence)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot marshal xml structure")
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedPresence xmpp.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 string `xml:"show"`
|
|
||||||
Status string `xml:"status"`
|
|
||||||
Priority string `xml:"priority"`
|
|
||||||
}
|
|
||||||
|
|
||||||
presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en")
|
|
||||||
presence.Show = "xa"
|
|
||||||
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 (%s)", parsedPresence.Priority)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MsgExtension 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMsgExtension returns an instance of MsgExtension, by matching packet type and XML
|
|
||||||
// tag name against the registry.
|
|
||||||
func (r *registry) GetMsgExtension(name xml.Name) MsgExtension {
|
|
||||||
if extensionType := r.GetExtensionType(PKTMessage, name); extensionType != nil {
|
|
||||||
val := reflect.New(extensionType)
|
|
||||||
elt := val.Interface()
|
|
||||||
if msgExt, ok := elt.(MsgExtension); ok {
|
|
||||||
return msgExt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIQExtension returns an instance of IQPayload, by matching packet type and XML
|
|
||||||
// tag name against the registry.
|
|
||||||
func (r *registry) GetIQExtension(name xml.Name) IQPayload {
|
|
||||||
if extensionType := r.GetExtensionType(PKTIQ, name); extensionType != nil {
|
|
||||||
val := reflect.New(extensionType)
|
|
||||||
elt := val.Interface()
|
|
||||||
if iqExt, ok := elt.(IQPayload); ok {
|
|
||||||
return iqExt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-193
@@ -1,193 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 StreamFeatures
|
|
||||||
TlsEnabled bool
|
|
||||||
lastPacketId int
|
|
||||||
|
|
||||||
// Session interface
|
|
||||||
In chan interface{}
|
|
||||||
Out chan interface{}
|
|
||||||
|
|
||||||
// read / write
|
|
||||||
socketProxy io.ReadWriter
|
|
||||||
decoder *xml.Decoder
|
|
||||||
|
|
||||||
// error management
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
|
|
||||||
s := new(Session)
|
|
||||||
s.init(conn, o)
|
|
||||||
|
|
||||||
// starttls
|
|
||||||
var tlsConn net.Conn
|
|
||||||
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain)
|
|
||||||
if s.TlsEnabled {
|
|
||||||
s.reset(conn, tlsConn, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.TlsEnabled && !o.Insecure {
|
|
||||||
return nil, nil, NewConnError(errors.New("failed to negotiate TLS session"), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// auth
|
|
||||||
s.auth(o)
|
|
||||||
s.reset(tlsConn, tlsConn, o)
|
|
||||||
|
|
||||||
// bind resource and 'start' XMPP session
|
|
||||||
s.bind(o)
|
|
||||||
s.rfc3921Session(o)
|
|
||||||
|
|
||||||
return tlsConn, s, s.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) PacketId() string {
|
|
||||||
s.lastPacketId++
|
|
||||||
return fmt.Sprintf("%x", s.lastPacketId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) init(conn net.Conn, o Config) {
|
|
||||||
s.setProxy(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.setProxy(conn, newConn, o)
|
|
||||||
s.Features = s.open(o.parsedJid.Domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: setProxyLogger ? better name ? This is not a TCP / HTTP proxy
|
|
||||||
func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Config) {
|
|
||||||
if newConn != conn {
|
|
||||||
s.socketProxy = newSocketProxy(newConn, o.PacketLogger)
|
|
||||||
}
|
|
||||||
s.decoder = xml.NewDecoder(s.socketProxy)
|
|
||||||
s.decoder.CharsetReader = o.CharsetReader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) open(domain string) (f StreamFeatures) {
|
|
||||||
// Send stream open tag
|
|
||||||
if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, NSClient, NSStream); s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set xml decoder and extract streamID from reply
|
|
||||||
s.StreamId, s.err = initDecoder(s.decoder) // TODO refactor / rename
|
|
||||||
if s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract stream features
|
|
||||||
if s.err = s.decoder.Decode(&f); s.err != nil {
|
|
||||||
s.err = errors.New("stream open decode features: " + s.err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
|
||||||
if s.err != nil {
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := s.Features.DoesStartTLS(); ok {
|
|
||||||
fmt.Fprintf(s.socketProxy, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
|
||||||
|
|
||||||
var k tlsProceed
|
|
||||||
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
|
|
||||||
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
s.TlsEnabled = true
|
|
||||||
|
|
||||||
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
|
|
||||||
DefaultTlsConfig.ServerName = domain
|
|
||||||
tlsConn := tls.Client(conn, &DefaultTlsConfig)
|
|
||||||
// We convert existing connection to TLS
|
|
||||||
if s.err = tlsConn.Handshake(); s.err != nil {
|
|
||||||
return tlsConn
|
|
||||||
}
|
|
||||||
|
|
||||||
// We check that cert matches hostname
|
|
||||||
s.err = tlsConn.VerifyHostname(domain)
|
|
||||||
return tlsConn
|
|
||||||
}
|
|
||||||
|
|
||||||
// starttls is not supported => we do not upgrade the connection:
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) auth(o Config) {
|
|
||||||
if s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.err = authSASL(s.socketProxy, 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.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
|
||||||
s.PacketId(), nsBind, resource)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), nsBind)
|
|
||||||
}
|
|
||||||
|
|
||||||
var iq 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[0].(type) {
|
|
||||||
case *BindBind:
|
|
||||||
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
|
|
||||||
default:
|
|
||||||
s.err = errors.New("iq bind result missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove when ejabberd is fixed: https://github.com/processone/ejabberd/issues/869
|
|
||||||
// After the bind, if the session is required (as per old RFC 3921), we send the session open iq
|
|
||||||
func (s *Session) rfc3921Session(o Config) {
|
|
||||||
if s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var iq IQ
|
|
||||||
if s.Features.Session.optional.Local != "" {
|
|
||||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), nsSession)
|
|
||||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
|
||||||
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mediated Read / Write on socket
|
|
||||||
// Used if logFile from Config is not nil
|
|
||||||
type socketProxy struct {
|
|
||||||
socket io.ReadWriter // Actual connection
|
|
||||||
logFile *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSocketProxy(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
|
|
||||||
if logFile == nil {
|
|
||||||
return conn
|
|
||||||
} else {
|
|
||||||
return &socketProxy{conn, logFile}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sp *socketProxy) 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 *socketProxy) 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
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DefaultTlsConfig tls.Config
|
|
||||||
|
|
||||||
// Used during stream initiation / session establishment
|
|
||||||
type tlsProceed struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type tlsFailure struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// StreamFeatures Packet
|
|
||||||
// Reference: https://xmpp.org/registrar/stream-features.html
|
|
||||||
|
|
||||||
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 BindBind
|
|
||||||
Session sessionSession
|
|
||||||
StreamManagement streamManagement
|
|
||||||
// ProcessOne Stream Features
|
|
||||||
P1Push p1Push
|
|
||||||
P1Rebind p1Rebind
|
|
||||||
p1Ack p1Ack
|
|
||||||
Any []xml.Name `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (StreamFeatures) Name() string {
|
|
||||||
return "stream:features"
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamFeatureDecoder struct{}
|
|
||||||
|
|
||||||
var streamFeatures streamFeatureDecoder
|
|
||||||
|
|
||||||
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
|
|
||||||
var packet StreamFeatures
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capabilities
|
|
||||||
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
|
|
||||||
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
|
|
||||||
// and peer servers do not need to send service discovery requests each time they connect."
|
|
||||||
// This is not a stream feature but a way to let client cache server disco info.
|
|
||||||
type Caps struct {
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
|
|
||||||
Hash string `xml:"hash,attr"`
|
|
||||||
Node string `xml:"node,attr"`
|
|
||||||
Ver string `xml:"ver,attr"`
|
|
||||||
Ext string `xml:"ext,attr,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Supported Stream Features
|
|
||||||
|
|
||||||
// StartTLS feature
|
|
||||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
|
|
||||||
type tlsStartTLS struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing startTLS required flag
|
|
||||||
func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
stls.XMLName = start.Name
|
|
||||||
|
|
||||||
// Check subelements to extract required field as boolean
|
|
||||||
for {
|
|
||||||
t, err := d.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tt := t.(type) {
|
|
||||||
|
|
||||||
case xml.StartElement:
|
|
||||||
elt := new(Node)
|
|
||||||
|
|
||||||
err = d.DecodeElement(elt, &tt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if elt.XMLName.Local == "required" {
|
|
||||||
stls.Required = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case xml.EndElement:
|
|
||||||
if tt == start.End() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStartTLS() (feature tlsStartTLS, isSupported bool) {
|
|
||||||
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
|
||||||
return sf.StartTLS, true
|
|
||||||
}
|
|
||||||
return feature, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mechanisms
|
|
||||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
|
|
||||||
type saslMechanisms struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
|
||||||
Mechanism []string `xml:"mechanism"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamManagement
|
|
||||||
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
|
|
||||||
type streamManagement struct {
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
|
||||||
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// P1 extensions
|
|
||||||
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
|
|
||||||
|
|
||||||
// p1:push support
|
|
||||||
type p1Push struct {
|
|
||||||
XMLName xml.Name `xml:"p1:push push"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1:rebind suppor
|
|
||||||
type p1Rebind struct {
|
|
||||||
XMLName xml.Name `xml:"p1:rebind rebind"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1:ack support
|
|
||||||
type p1Ack struct {
|
|
||||||
XMLName xml.Name `xml:"p1:ack ack"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// StreamError Packet
|
|
||||||
|
|
||||||
type StreamError struct {
|
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
|
|
||||||
Error xml.Name `xml:",any"`
|
|
||||||
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (StreamError) Name() string {
|
|
||||||
return "stream:error"
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamErrorDecoder struct{}
|
|
||||||
|
|
||||||
var streamError streamErrorDecoder
|
|
||||||
|
|
||||||
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
|
|
||||||
var packet StreamError
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/xmpp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
type StreamClient interface {
|
|
||||||
Connect() error
|
|
||||||
Disconnect()
|
|
||||||
SetHandler(handler EventHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostConnect func(c StreamClient)
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start launch the connection loop
|
|
||||||
func (sm *StreamManager) Start() 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)
|
|
||||||
|
|
||||||
return sm.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
|
||||||
func (sm *StreamManager) connect() error {
|
|
||||||
var backoff Backoff // TODO: Group backoff calculation features with connection manager?
|
|
||||||
|
|
||||||
for {
|
|
||||||
var err error
|
|
||||||
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
|
||||||
sm.Metrics = initMetrics()
|
|
||||||
|
|
||||||
if err = sm.client.Connect(); err != nil {
|
|
||||||
var actualErr ConnError
|
|
||||||
if xerrors.As(err, &actualErr) {
|
|
||||||
if actualErr.Permanent {
|
|
||||||
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
backoff.Wait()
|
|
||||||
} else { // We are connected, we can leave the retry loop
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sm.PostConnect != nil {
|
|
||||||
sm.PostConnect(sm.client)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream Metrics
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type Metrics struct {
|
|
||||||
startTime time.Time
|
|
||||||
// ConnectTime returns the duration between client initiation of the TCP/IP
|
|
||||||
// connection to the server and actual TCP/IP session establishment.
|
|
||||||
// This time includes DNS resolution and can be slightly higher if the DNS
|
|
||||||
// resolution result was not in cache.
|
|
||||||
ConnectTime time.Duration
|
|
||||||
// LoginTime returns the between client initiation of the TCP/IP
|
|
||||||
// connection to the server and the return of the login result.
|
|
||||||
// This includes ConnectTime, but also XMPP level protocol negociation
|
|
||||||
// like starttls.
|
|
||||||
LoginTime time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// initMetrics set metrics with default value and define the starting point
|
|
||||||
// for duration calculation (connect time, login time, etc).
|
|
||||||
func initMetrics() *Metrics {
|
|
||||||
return &Metrics{
|
|
||||||
startTime: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Metrics) setConnectTime() {
|
|
||||||
m.ConnectTime = time.Since(m.startTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Metrics) setLoginTime() {
|
|
||||||
m.LoginTime = time.Since(m.startTime)
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package xmpp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNoStartTLS(t *testing.T) {
|
|
||||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
|
||||||
</stream:features>`
|
|
||||||
|
|
||||||
var parsedSF xmpp.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 xmpp.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 xmpp.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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package xmpp // import "gosrc.io/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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
export GO111MODULE=on
|
|
||||||
echo "" > coverage.txt
|
|
||||||
|
|
||||||
for d in $(go list ./... | grep -v vendor); do
|
|
||||||
go test -race -coverprofile=profile.out -covermode=atomic ${d}
|
|
||||||
if [ -f profile.out ]; then
|
|
||||||
cat profile.out >> coverage.txt
|
|
||||||
rm profile.out
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -f "./codecov.sh" ]; then
|
|
||||||
./codecov.sh
|
|
||||||
fi
|
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data"
|
||||||
|
XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clientAvatarData struct {
|
||||||
|
XMLName xml.Name `xml:"data"`
|
||||||
|
Data []byte `xml:",innerxml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientAvatarInfo struct {
|
||||||
|
XMLName xml.Name `xml:"info"`
|
||||||
|
Bytes string `xml:"bytes,attr"`
|
||||||
|
Width string `xml:"width,attr"`
|
||||||
|
Height string `xml:"height,attr"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
URL string `xml:"url,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientAvatarMetadata struct {
|
||||||
|
XMLName xml.Name `xml:"metadata"`
|
||||||
|
XMLNS string `xml:"xmlns,attr"`
|
||||||
|
Info clientAvatarInfo `xml:"info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AvatarData struct {
|
||||||
|
Data []byte
|
||||||
|
From string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AvatarMetadata struct {
|
||||||
|
From string
|
||||||
|
Bytes int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
|
||||||
|
var data clientAvatarData
|
||||||
|
err := xml.Unmarshal(itemsBody, &data)
|
||||||
|
if err != nil {
|
||||||
|
return AvatarData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64-decode the avatar data to check its SHA1 hash
|
||||||
|
dataRaw, err := base64.StdEncoding.DecodeString(
|
||||||
|
string(data.Data))
|
||||||
|
if err != nil {
|
||||||
|
return AvatarData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha1.Sum(dataRaw)
|
||||||
|
hashStr := hex.EncodeToString(hash[:])
|
||||||
|
if hashStr != id {
|
||||||
|
return AvatarData{}, errors.New("SHA1 hashes do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
return AvatarData{
|
||||||
|
Data: dataRaw,
|
||||||
|
From: from,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) {
|
||||||
|
var meta clientAvatarMetadata
|
||||||
|
err := xml.Unmarshal(body, &meta)
|
||||||
|
if err != nil {
|
||||||
|
return AvatarMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return AvatarMetadata{
|
||||||
|
From: from,
|
||||||
|
Bytes: atoiw(meta.Info.Bytes),
|
||||||
|
Width: atoiw(meta.Info.Width),
|
||||||
|
Height: atoiw(meta.Info.Height),
|
||||||
|
ID: meta.Info.ID,
|
||||||
|
Type: meta.Info.Type,
|
||||||
|
URL: meta.Info.URL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wrapper for atoi which just returns -1 if an error occurs
|
||||||
|
func atoiw(str string) int {
|
||||||
|
i, err := strconv.Atoi(str)
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AvatarSubscribeMetadata(jid string) {
|
||||||
|
c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AvatarUnsubscribeMetadata(jid string) {
|
||||||
|
c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AvatarRequestData(jid string) {
|
||||||
|
c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AvatarRequestDataByID(jid, id string) {
|
||||||
|
c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AvatarRequestMetadata(jid string) {
|
||||||
|
c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
|
||||||
|
XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clientDiscoFeature struct {
|
||||||
|
XMLName xml.Name `xml:"feature"`
|
||||||
|
Var string `xml:"var,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientDiscoIdentity struct {
|
||||||
|
XMLName xml.Name `xml:"identity"`
|
||||||
|
Category string `xml:"category,attr"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientDiscoQuery struct {
|
||||||
|
XMLName xml.Name `xml:"query"`
|
||||||
|
Features []clientDiscoFeature `xml:"feature"`
|
||||||
|
Identities []clientDiscoIdentity `xml:"identity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientDiscoItem struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientDiscoItemsQuery struct {
|
||||||
|
XMLName xml.Name `xml:"query"`
|
||||||
|
Items []clientDiscoItem `xml:"item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoIdentity struct {
|
||||||
|
Category string
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoItem struct {
|
||||||
|
Jid string
|
||||||
|
Name string
|
||||||
|
Node string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoResult struct {
|
||||||
|
Features []string
|
||||||
|
Identities []DiscoIdentity
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoItems struct {
|
||||||
|
Jid string
|
||||||
|
Items []DiscoItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientFeaturesToReturn(features []clientDiscoFeature) []string {
|
||||||
|
var ret []string
|
||||||
|
|
||||||
|
for _, feature := range features {
|
||||||
|
ret = append(ret, feature.Var)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientIdentitiesToReturn(identities []clientDiscoIdentity) []DiscoIdentity {
|
||||||
|
var ret []DiscoIdentity
|
||||||
|
|
||||||
|
for _, id := range identities {
|
||||||
|
ret = append(ret, DiscoIdentity{
|
||||||
|
Category: id.Category,
|
||||||
|
Type: id.Type,
|
||||||
|
Name: id.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem {
|
||||||
|
var ret []DiscoItem
|
||||||
|
for _, item := range items {
|
||||||
|
ret = append(ret, DiscoItem{
|
||||||
|
Jid: item.Jid,
|
||||||
|
Name: item.Name,
|
||||||
|
Node: item.Node,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IQTypeGet = "get"
|
||||||
|
IQTypeSet = "set"
|
||||||
|
IQTypeResult = "result"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) Discovery() (string, error) {
|
||||||
|
// use UUIDv4 for a pseudo random id.
|
||||||
|
reqID := strconv.FormatUint(uint64(getCookie()), 10)
|
||||||
|
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover information about a node
|
||||||
|
func (c *Client) DiscoverNodeInfo(node string) (string, error) {
|
||||||
|
query := fmt.Sprintf("<query xmlns='%s' node='%s'/>", XMPPNS_DISCO_INFO, node)
|
||||||
|
return c.RawInformation(c.jid, c.domain, "info3", IQTypeGet, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover items that the server exposes
|
||||||
|
func (c *Client) DiscoverServerItems() (string, error) {
|
||||||
|
return c.DiscoverEntityItems(c.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover items that an entity exposes
|
||||||
|
func (c *Client) DiscoverEntityItems(jid string) (string, error) {
|
||||||
|
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_ITEMS)
|
||||||
|
return c.RawInformation(c.jid, jid, "info1", IQTypeGet, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawInformationQuery sends an information query request to the server.
|
||||||
|
func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) {
|
||||||
|
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'><query xmlns='%s'>%s</query></iq>\n"
|
||||||
|
_, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawInformation send a IQ request with the payload body to the server
|
||||||
|
func (c *Client) RawInformation(from, to, id, iqType, body string) (string, error) {
|
||||||
|
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'>%s</iq>\n"
|
||||||
|
_, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright 2013 Flo Lauber <dev@qatfy.at>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// TODO(flo):
|
||||||
|
// - support password protected MUC rooms
|
||||||
|
// - cleanup signatures of join/leave functions
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nsMUC = "http://jabber.org/protocol/muc"
|
||||||
|
nsMUCUser = "http://jabber.org/protocol/muc#user"
|
||||||
|
NoHistory = 0
|
||||||
|
CharHistory = 1
|
||||||
|
StanzaHistory = 2
|
||||||
|
SecondsHistory = 3
|
||||||
|
SinceHistory = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send sends room topic wrapped inside an XMPP message stanza body.
|
||||||
|
func (c *Client) SendTopic(chat Chat) (n int, err error) {
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<message to='%s' type='%s' xml:lang='en'>"+"<subject>%s</subject></message>\n",
|
||||||
|
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
|
||||||
|
if nick == "" {
|
||||||
|
nick = c.jid
|
||||||
|
}
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<history maxchars='0'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// xep-0045 7.2
|
||||||
|
func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) {
|
||||||
|
if nick == "" {
|
||||||
|
nick = c.jid
|
||||||
|
}
|
||||||
|
switch history_type {
|
||||||
|
case NoHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s' />"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
||||||
|
case CharHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<history maxchars='%d'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
||||||
|
case StanzaHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<history maxstanzas='%d'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
||||||
|
case SecondsHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<history seconds='%d'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
||||||
|
case SinceHistory:
|
||||||
|
if history_date != nil {
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<history since='%s'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, errors.New("Unknown history option")
|
||||||
|
}
|
||||||
|
|
||||||
|
// xep-0045 7.2.6
|
||||||
|
func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) {
|
||||||
|
if nick == "" {
|
||||||
|
nick = c.jid
|
||||||
|
}
|
||||||
|
switch history_type {
|
||||||
|
case NoHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<password>%s</password>"+
|
||||||
|
"</x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
|
||||||
|
case CharHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<password>%s</password>"+
|
||||||
|
"<history maxchars='%d'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
||||||
|
case StanzaHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<password>%s</password>"+
|
||||||
|
"<history maxstanzas='%d'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
||||||
|
case SecondsHistory:
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<password>%s</password>"+
|
||||||
|
"<history seconds='%d'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
||||||
|
case SinceHistory:
|
||||||
|
if history_date != nil {
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence to='%s/%s'>"+
|
||||||
|
"<x xmlns='%s'>"+
|
||||||
|
"<password>%s</password>"+
|
||||||
|
"<history since='%s'/></x>"+
|
||||||
|
"</presence>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, errors.New("Unknown history option")
|
||||||
|
}
|
||||||
|
|
||||||
|
// xep-0045 7.14
|
||||||
|
func (c *Client) LeaveMUC(jid string) (n int, err error) {
|
||||||
|
return fmt.Fprintf(c.stanzaWriter, "<presence from='%s' to='%s' type='unavailable' />\n",
|
||||||
|
c.jid, xmlEscape(jid))
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) PingC2S(jid, server string) error {
|
||||||
|
if jid == "" {
|
||||||
|
jid = c.jid
|
||||||
|
}
|
||||||
|
if server == "" {
|
||||||
|
server = c.domain
|
||||||
|
}
|
||||||
|
_, err := fmt.Fprintf(c.stanzaWriter, "<iq from='%s' to='%s' id='c2s1' type='get'>"+
|
||||||
|
"<ping xmlns='urn:xmpp:ping'/>"+
|
||||||
|
"</iq>\n",
|
||||||
|
xmlEscape(jid), xmlEscape(server))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PingS2S(fromServer, toServer string) error {
|
||||||
|
_, err := fmt.Fprintf(c.stanzaWriter, "<iq from='%s' to='%s' id='s2s1' type='get'>"+
|
||||||
|
"<ping xmlns='urn:xmpp:ping'/>"+
|
||||||
|
"</iq>\n",
|
||||||
|
xmlEscape(fromServer), xmlEscape(toServer))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendResultPing(id, toServer string) error {
|
||||||
|
_, err := fmt.Fprintf(c.stanzaWriter, "<iq type='result' to='%s' id='%s'/>\n",
|
||||||
|
xmlEscape(toServer), xmlEscape(id))
|
||||||
|
return err
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub"
|
||||||
|
XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clientPubsubItem struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
Body []byte `xml:",innerxml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientPubsubItems struct {
|
||||||
|
XMLName xml.Name `xml:"items"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Items []clientPubsubItem `xml:"item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientPubsubEvent struct {
|
||||||
|
XMLName xml.Name `xml:"event"`
|
||||||
|
XMLNS string `xml:"xmlns,attr"`
|
||||||
|
Items clientPubsubItems `xml:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientPubsubError struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientPubsubSubscription struct {
|
||||||
|
XMLName xml.Name `xml:"subscription"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
JID string `xml:"jid,attr"`
|
||||||
|
SubID string `xml:"subid,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PubsubEvent struct {
|
||||||
|
Node string
|
||||||
|
Items []PubsubItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type PubsubSubscription struct {
|
||||||
|
SubID string
|
||||||
|
JID string
|
||||||
|
Node string
|
||||||
|
Errors []string
|
||||||
|
}
|
||||||
|
type PubsubUnsubscription PubsubSubscription
|
||||||
|
|
||||||
|
type PubsubItem struct {
|
||||||
|
ID string
|
||||||
|
InnerXML []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type PubsubItems struct {
|
||||||
|
Node string
|
||||||
|
Items []PubsubItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts []clientPubsubItem to []PubsubItem
|
||||||
|
func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem {
|
||||||
|
var tmp []PubsubItem
|
||||||
|
for _, i := range items {
|
||||||
|
tmp = append(tmp, PubsubItem{
|
||||||
|
ID: i.ID,
|
||||||
|
InnerXML: i.Body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubsubClientToReturn(event clientPubsubEvent) PubsubEvent {
|
||||||
|
return PubsubEvent{
|
||||||
|
Node: event.Items.Node,
|
||||||
|
Items: pubsubItemsToReturn(event.Items.Items),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubsubStanza(body string) string {
|
||||||
|
return fmt.Sprintf("<pubsub xmlns='%s'>%s</pubsub>",
|
||||||
|
XMPPNS_PUBSUB, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubsubSubscriptionStanza(node, jid string) string {
|
||||||
|
body := fmt.Sprintf("<subscribe node='%s' jid='%s'/>",
|
||||||
|
xmlEscape(node),
|
||||||
|
xmlEscape(jid))
|
||||||
|
return pubsubStanza(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubsubUnsubscriptionStanza(node, jid string) string {
|
||||||
|
body := fmt.Sprintf("<unsubscribe node='%s' jid='%s'/>",
|
||||||
|
xmlEscape(node),
|
||||||
|
xmlEscape(jid))
|
||||||
|
return pubsubStanza(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PubsubSubscribeNode(node, jid string) {
|
||||||
|
c.RawInformation(c.jid,
|
||||||
|
jid,
|
||||||
|
"sub1",
|
||||||
|
"set",
|
||||||
|
pubsubSubscriptionStanza(node, c.jid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PubsubUnsubscribeNode(node, jid string) {
|
||||||
|
c.RawInformation(c.jid,
|
||||||
|
jid,
|
||||||
|
"unsub1",
|
||||||
|
"set",
|
||||||
|
pubsubUnsubscriptionStanza(node, c.jid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PubsubRequestLastItems(node, jid string) {
|
||||||
|
body := fmt.Sprintf("<items node='%s'/>", node)
|
||||||
|
c.RawInformation(c.jid, jid, "items1", "get", pubsubStanza(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PubsubRequestItem(node, jid, id string) {
|
||||||
|
body := fmt.Sprintf("<items node='%s'><item id='%s'/></items>", node, id)
|
||||||
|
c.RawInformation(c.jid, jid, "items3", "get", pubsubStanza(body))
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) ApproveSubscription(jid string) {
|
||||||
|
fmt.Fprintf(c.stanzaWriter, "<presence to='%s' type='subscribed'/>\n",
|
||||||
|
xmlEscape(jid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RevokeSubscription(jid string) {
|
||||||
|
fmt.Fprintf(c.stanzaWriter, "<presence to='%s' type='unsubscribed'/>\n",
|
||||||
|
xmlEscape(jid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RetrieveSubscription(jid string) {
|
||||||
|
fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribe'/>\n",
|
||||||
|
xmlEscape(jid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RequestSubscription(jid string) {
|
||||||
|
fmt.Fprintf(c.stanzaWriter, "<presence to='%s' type='subscribe'/>\n",
|
||||||
|
xmlEscape(jid))
|
||||||
|
}
|
||||||
+135
-21
@@ -1,29 +1,143 @@
|
|||||||
package xmpp_test
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
"github.com/google/go-cmp/cmp"
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compare iq structure but ignore empty namespace as they are set properly on
|
type localAddr struct{}
|
||||||
// marshal / unmarshal. There is no need to manage them on the manually
|
|
||||||
// crafted structure.
|
func (a *localAddr) Network() string {
|
||||||
func xmlEqual(x, y interface{}) bool {
|
return "tcp"
|
||||||
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
|
}
|
||||||
opts := cmp.Options{
|
|
||||||
cmp.FilterValues(func(x, y interface{}) bool {
|
func (addr *localAddr) String() string {
|
||||||
xx, xok := x.(xml.Name)
|
return "localhost:5222"
|
||||||
yy, yok := y.(xml.Name)
|
}
|
||||||
if xok && yok {
|
|
||||||
zero := xml.Name{}
|
type testConn struct {
|
||||||
if xx == zero || yy == zero {
|
*bytes.Buffer
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
}
|
func tConnect(s string) net.Conn {
|
||||||
return false
|
var conn testConn
|
||||||
}, alwaysEqual),
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmp.Equal(x, y, opts)
|
chat := Chat{
|
||||||
|
Type: "error",
|
||||||
|
Other: []string{
|
||||||
|
"\n\t\t{\"random\": \"<text>\"}\n\t",
|
||||||
|
"\n\t\t\n\t\t\n\t",
|
||||||
|
},
|
||||||
|
OtherElem: []XMLElement{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "google:mobile:data", Local: "gcm"},
|
||||||
|
Attr: []xml.Attr{{Name: xml.Name{Space: "", Local: "xmlns"}, Value: "google:mobile:data"}},
|
||||||
|
InnerXML: "\n\t\t{\"random\": \"<text>\"}\n\t",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "jabber:client", Local: "error"},
|
||||||
|
Attr: []xml.Attr{{Name: xml.Name{Space: "", Local: "code"}, Value: "400"}, {Name: xml.Name{Space: "", Local: "type"}, Value: "modify"}},
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var emptyPubSub = strings.TrimSpace(`
|
||||||
|
<iq xmlns="jabber:client" type='result' from='juliet@capulet.lit' id='items3'>
|
||||||
|
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||||
|
<items node='urn:xmpp:avatar:data'></items>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TestEmptyPubsub(t *testing.T) {
|
||||||
|
var c Client
|
||||||
|
c.conn = tConnect(emptyPubSub)
|
||||||
|
c.p = xml.NewDecoder(c.conn)
|
||||||
|
m, err := c.Recv()
|
||||||
|
|
||||||
|
switch m.(type) {
|
||||||
|
case AvatarData:
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error to be returned")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("Recv() = %v", m)
|
||||||
|
t.Errorf("Expected a return value of AvatarData")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user