331 Commits

Author SHA1 Message Date
Mickael Remond 1c4dd6c967 Remove debug print-out 2019-07-31 18:54:49 +02:00
Mickael Remond 1b3dec3902 Clean-up: remove test/debug code 2019-07-31 18:51:16 +02:00
Mickael Remond 3f48672946 Add initial support for stream management
For now it support enabling SM, replying to ack requests from server,
and trying resuming the session with existing Stream Management state.
2019-07-31 18:47:30 +02:00
Mickael Remond e531370dc9 An invalid certificate is a permanent error if we do not skip cert check 2019-07-31 11:43:54 +02:00
Mickael Remond 4e185f4bb6 Use intermediate version (before 0.2.0) to fix stanza package usage 2019-07-30 10:55:49 +02:00
Mickael Remond 4f1e0ded97 Simplify disco with builder helpers 2019-07-30 10:45:20 +02:00
Mickael Remond 176dcdce33 Simplify disco and software version
Make use of helpers.
2019-07-30 10:45:20 +02:00
Mickael Remond 61adf7e414 Add builder & test on software version helpers 2019-07-30 10:45:20 +02:00
Mickael Remond 014957e029 Expand comments 2019-07-30 10:45:20 +02:00
Mickael Remond 69118a952a Add helpers for IQ DiscoItems 2019-07-30 10:45:20 +02:00
Mickael Remond 1c74d102c7 Fix reference to missing tag 2019-07-30 10:39:19 +02:00
Mickael Remond 7ab6c3a62d Refactor to start removing global variables 2019-07-27 18:06:55 -07:00
Mickael Remond a3867dd0b3 Expand TODO list 2019-07-27 17:50:45 -07:00
Mickael Remond d2a1329dc6 Report errors 2019-07-27 17:50:28 -07:00
Mickael Remond 6ff7812ac4 go mod tidy 2019-07-27 17:34:10 -07:00
Mickael Remond 3453336f27 For now we need to use master version for xmpp module 2019-07-27 17:31:11 -07:00
Mickael Remond a23194ad96 Add submodule for commands
The goal is to keep dependencies list minimal for users of the xmpp
modules. We do not want to force to increase largely the number of
indirect dependencies when you require xmpp.

The command-line stuff may not likely be needed in the end developer
application.
2019-07-27 17:15:28 -07:00
Mickael Remond f984a93e63 Formatting 2019-07-27 16:50:41 -07:00
Mickael Remond 6a5f2750f1 Clean-up 2019-07-27 16:50:10 -07:00
Mickaël Rémond e553028754 Minor wording fixes 2019-07-27 16:36:35 -07:00
Mickaël Rémond fed23ad7ad Minor improvements for sendxmpp doc 2019-07-27 16:36:35 -07:00
Mickaël Rémond 244acdc02a Fix typos 2019-07-27 16:36:35 -07:00
Mickaël Rémond 4d6c783619 Improve wording 2019-07-27 16:36:35 -07:00
Martin/Geno 5697d40e5c use - instatt of --stdin to detect stdin 2019-07-27 16:36:35 -07:00
genofire ff5885f29d todo for sendxmpp 2019-07-27 16:36:35 -07:00
Martin/Geno e3e57ac803 add parameter and config for address to sendxmpp 2019-07-27 16:36:35 -07:00
Martin/Geno 3daa5c505c fix README.md 2019-07-27 16:36:35 -07:00
Martin/Geno 0fb90abcf7 improve authentification 2019-07-27 16:36:35 -07:00
Martin/Geno 6aa942dd58 first idea of sendxmpp 2019-07-27 16:36:35 -07:00
Mickael Remond c41d068c9f Improve comments 2019-07-27 15:19:32 -07:00
Mickael Remond 9f095cb90f Update dependencies 2019-07-27 09:22:44 -07:00
Mickael Remond 7deaf59642 Quickfix for build error
See #94
2019-07-27 09:22:04 -07:00
genofire fe6cea870d use highest DNS-SRV entry for client connection 2019-07-27 09:11:00 -07:00
Martin/Geno 323de704f6 improve command xmpp-check 2019-07-17 10:41:49 +02:00
Martin/Geno e05f36c69f init empty TLSConfig, if nothing given 2019-07-16 11:00:42 +02:00
Mickael Remond d36428fb2f Avoid copying tls.Config lock
Fixes #90
2019-07-15 18:40:20 +02:00
Mickael Remond 9577036327 Add support for self-signed certificates 2019-07-15 12:22:21 +02:00
Mickael Remond 79803a8af9 Improves comments 2019-06-29 17:52:36 +02:00
Mickael Remond 604d2c6c1e Improves comments 2019-06-29 17:48:38 +02:00
Mickael Remond 7c71d93026 Remove unused channels 2019-06-29 17:39:59 +02:00
Mickael Remond cca0919b8a Fix session element parsing on IQ 2019-06-29 17:39:19 +02:00
Mickael Remond 40e907e8ee Clean-up & refactor 2019-06-29 16:49:54 +02:00
Mickael Remond 838c059398 Remove Bind in the payload list, as it is intended to be used by XMPP client and not by users of the library. 2019-06-29 16:10:53 +02:00
Mickael Remond 3ba59afd6e Start listing supported specifications 2019-06-29 15:15:09 +02:00
Mickael Remond 661188752e Formatting 2019-06-29 15:09:05 +02:00
Mickael Remond 409d563eec Update README example after API change 2019-06-29 14:58:59 +02:00
Mickael Remond d90cc239ae Spelling consistency 2019-06-29 14:57:24 +02:00
Mickaël Rémond b35868b689 Improve stanza package documentation 2019-06-29 14:53:14 +02:00
Mickael Remond 6165232d7a Improves documentation of stanza package 2019-06-29 14:40:35 +02:00
Mickael Remond 91c562200d Move missing file to stanza package 2019-06-29 11:03:55 +02:00
Mickael Remond 5992cc2231 Fix XMPP logger consistency
- Rename socketProxy to StreamLogger
- Ensure client send traffic through the logger
2019-06-29 10:47:07 +02:00
Mickael Remond 318e5e8a50 Postconnect method should receive an xmpp.Sender and not directly a client
Fixes #80
2019-06-29 09:35:33 +02:00
Mickael Remond a465e370e2 Rename check_cert 2019-06-29 09:17:35 +02:00
Mickael Remond 9bb4f32769 Clean up & documentation 2019-06-28 16:41:53 +02:00
Mickael Remond e3c0747cbb Improves documentation: Explain how to create a custom stanza extension 2019-06-28 16:19:09 +02:00
Mickaël Rémond 0fd1bb2483 Merge pull request #79: Stanza package & pattern to help building stanzas
- Move parsing and stanza marshalling / unmarshalling to stanza package
- Add pattern & basic helpers to simplify stanza building.
This was requested on #61
2019-06-27 14:57:26 +02:00
Mickael Remond 4a4fc39cf6 Merge with changes from master 2019-06-27 14:55:44 +02:00
Mickael Remond 5db9a80605 Move example to new data structure 2019-06-27 14:35:03 +02:00
Mickael Remond 20a66dc47d Use an approach to build stanza that do not require a "builder" abstraction 2019-06-27 14:30:23 +02:00
Mickael Remond 1dacc663d3 Add basic builder support 2019-06-27 10:23:49 +02:00
Mickael Remond cb9016693c Move some IQ declaration in their own files 2019-06-27 10:22:36 +02:00
Mickael Remond 0c7e4588c6 Add initial documentation 2019-06-27 10:21:33 +02:00
Mickael Remond 3fa1a4b387 Remove useless reference to IQPayload 2019-06-27 09:59:19 +02:00
Mickaël Rémond 80f32b4af7 Update README.md 2019-06-27 09:47:08 +02:00
Mickael Remond 781b875cf1 Resync with Master
Support NullableInt on MUC presence history element
2019-06-26 18:42:40 +02:00
Mickael Remond 3d088a6078 Use NullableInt to encode presence history values 2019-06-26 18:31:17 +02:00
Mickael Remond 0ee4764d31 Update error.go
Fix typo in comment
2019-06-26 18:31:17 +02:00
Martin/Geno 1971772394 fix everything 2019-06-26 18:31:17 +02:00
genofire 6fbfe9fd0a Update pres_muc_test.go 2019-06-26 18:31:17 +02:00
Mickael Remond 5ed66de79e Fix tests after refactor 2019-06-26 17:28:54 +02:00
Mickael Remond 428787d7ab Refactor and move parsing and stanza to a separate package 2019-06-26 17:14:52 +02:00
Mickael Remond 0acf824217 Fix typo in error const enum 2019-06-26 16:21:18 +02:00
Mickael Remond 445bb8efa3 Fix crash on send when disconnected
Fixes #74
2019-06-26 15:58:42 +02:00
Mickael Remond f79a3a219b Improves IPV6 examples 2019-06-26 14:00:39 +02:00
Mickael Remond 1c792e61c6 Adding tests and always use brackets in IPV6 addresses
Code also ensures that brackets are properly added when encoding an IPV6 address.
2019-06-26 12:37:59 +02:00
genofire fde524ef98 fix connection to ipv6 address + use fallback to jid domain 2019-06-26 12:37:59 +02:00
Mickael Remond 7a386ec8d0 Examples should use local repository version 2019-06-24 12:24:45 +02:00
Mickael Remond 83f96fbd41 Fix error code 2019-06-24 12:24:45 +02:00
Mickael Remond def9629a0b Make it possible to extract unknown iq payload, in field Any 2019-06-24 12:24:45 +02:00
Mickael Remond 1542110f1b If there is no match in router, properly send not-implemented reply for iq set & get 2019-06-24 12:24:45 +02:00
Mickael Remond d6d371df4d Do not export Router.route as it is not supposed to be called directly 2019-06-24 12:24:45 +02:00
Mickael Remond 3521c488ea Initial HTML message support 2019-06-23 15:53:24 +02:00
Mickael Remond 8f7b4ba8a4 Implement MUC Presence Extension
See #67
2019-06-23 12:21:56 +02:00
Mickael Remond 4a4c4850d1 Add msg.Get method to match and extract message extensions 2019-06-22 18:36:16 +02:00
Mickael Remond 6ddfa781e5 Update example in README 2019-06-22 11:29:47 +02:00
Mickael Remond 555cbe12b4 Update example dependency to latest code version 2019-06-22 11:28:01 +02:00
Mickael Remond e9c704eff5 Fix router after #62 merge 2019-06-22 11:24:14 +02:00
genofire d9fdff0839 Add constants (enumlike) for stanza types and simplify packet creation (#62)
* Add constants (enumlike) for stanza types
* NewIQ, NewMessage and NewPresence are now initialized with the Attrs struct
* Update examples
* Do not export backoff code. For now, we do not need to expose backoff in the documentation
* Make presence priority an int8
2019-06-22 11:13:33 +02:00
Mickaël Rémond 145fce6b3f Add StanzaType matcher / Clarify empty route behaviour (#65)
* Add route to match on stanza type

* Add test checking that an empty route "always" matches
2019-06-21 16:48:13 +02:00
Mickael Remond 5d362b505b Priority is an int 2019-06-20 18:36:57 +02:00
genofire 923fd61587 compress iq checking in component 2019-06-20 15:10:41 +02:00
genofire 44681e8053 fix iq - get after refactoring routing on #55 2019-06-20 15:10:41 +02:00
Mickael Remond 1a7aa94bae Update dependencies for examples 2019-06-19 14:33:14 +02:00
Mickael Remond a6cbc0c08f Properly decode an IQ with both a payload and an error 2019-06-19 14:03:42 +02:00
Mickael Remond 3f81465c6c Update examples 2019-06-19 14:03:42 +02:00
Mickael Remond 24502f7cd7 Expand test 2019-06-19 14:03:42 +02:00
Mickael Remond af0ae525b8 An IQ can only have a single payload
"An IQ stanza of type "get" or "set" MUST contain exactly one
 child element, which specifies the semantics of the particular
 request."
2019-06-19 14:03:42 +02:00
Mickaël Rémond d455f29258 Fix installation note 2019-06-19 11:43:16 +02:00
Mickael Remond 683fdea2ec Fix installation note 2019-06-18 17:18:17 +02:00
Mickael Remond 7f889909fd Add initial doc for xmpp-check 2019-06-18 17:01:26 +02:00
Mickael Remond 4d015e5b29 With go modules, we should be able to remove import comments 2019-06-18 16:28:30 +02:00
Mickael Remond c8ded1462f Fix import path 2019-06-18 16:13:52 +02:00
Mickael Remond 28ae759144 Fix import path 2019-06-18 16:11:00 +02:00
Mickael Remond 55c7251fac Fix import for go get 2019-06-18 15:33:37 +02:00
Mickael Remond 398ba224e7 Mention Namespace Delegation and Privileged Entity support 2019-06-18 15:16:19 +02:00
Mickael Remond 00e9dd4e47 Add link to examples directory. 2019-06-18 15:01:21 +02:00
Mickael Remond ddff6527bd Update examples dependencies 2019-06-18 14:39:58 +02:00
Mickael Remond 9219bf5aa9 Add namespace delegation and priviledged entity example 2019-06-18 14:36:56 +02:00
Mickael Remond 715bf6976f Fix client tests 2019-06-18 14:36:56 +02:00
Mickael Remond 348f29e055 Update example client to use router 2019-06-18 14:36:56 +02:00
Mickael Remond 45c7ca74b1 Make client use the new Router 2019-06-18 14:36:56 +02:00
Mickael Remond 7aef8357ed Clean-up 2019-06-18 14:36:56 +02:00
Mickael Remond 2c7b03fcea Clean-up 2019-06-18 14:36:56 +02:00
Mickael Remond 9b57809e9d Adapt examples to new routing library for components 2019-06-18 14:36:56 +02:00
Mickael Remond f0f0d5a285 Improve component README 2019-06-18 14:36:56 +02:00
Mickael Remond 61cdac89e0 Add support for generating delegation forwarded iq response 2019-06-18 14:36:56 +02:00
Mickael Remond c6f0d03f60 Add support for delegation namespace packet parsing
Refactor and clean up pubsub & pep files
2019-06-18 14:36:56 +02:00
Mickael Remond cc2fa7307f Ignore directory where I put private notes 2019-06-18 14:36:56 +02:00
Mickael Remond 9db33d5792 Introduce Sender interface to abstract client sending in router handlers 2019-06-18 14:36:56 +02:00
Mickael Remond b05e68c844 Add router to make it easier to set up routing info
- Using the router, the dispatch is not done anymore by receiving from
  receive channel, but by registering callback functions in routers,
  with matchers.
- Make IQPayload a real interface to make it easier to match namespaces.
- The StreamManager Run command is now blocking, waiting for StreamManager
  to terminate.
2019-06-18 14:36:56 +02:00
Mickaël Rémond f7b7482d2e Update README.md 2019-06-18 09:01:07 +02:00
genofire 355401aa84 wrong package import url let it failed 2019-06-18 08:58:39 +02:00
Mickael Remond eb54ec9fb1 Update Fluux XMPP version for examples 2019-06-11 15:31:28 +02:00
Mickaël Rémond 4d4710463d Add basic support for keep-alive (#48)
Fix #35 

This should also help with #8
2019-06-11 15:29:08 +02:00
Mickael Remond 2af9521036 Add support for detecting ProcessOne extensions 2019-06-11 09:20:33 +02:00
Mickael Remond 30e6adc073 Add support for detecting Stream Management 2019-06-10 16:36:47 +02:00
Mickael Remond 709a95129e Clean up and fix StartTLS feature discovery
Required field was never set to true
2019-06-10 16:27:52 +02:00
Mickael Remond 44568fcf2b Remove dead code
For now the component is not able to handle the discovery requests on its own.
2019-06-10 15:06:41 +02:00
Mickael Remond 08bb9965b8 Update component to advertise version feature and return it 2019-06-10 12:35:48 +02:00
Mickael Remond 322a6594e7 Fix missing entry in payload registry 2019-06-10 12:30:01 +02:00
Mickael Remond 45cb2e6f34 Add support for Software Version parsing 2019-06-10 11:56:07 +02:00
Mickael Remond 411619c2ef Make channel type more specific (Packet instead of interface{})
Thanks to Genofire for spotting this
2019-06-10 10:58:41 +02:00
Mickael Remond 36e3379f5a Update examples dependencies 2019-06-09 13:18:54 +02:00
Mickael Remond bc2fad6693 Let component handle discovery for now 2019-06-09 13:08:25 +02:00
Mickael Remond 909cf753c9 Fix missing default channel creation 2019-06-09 13:08:25 +02:00
Mickael Remond 83ae778d33 Return errors on SendRaw 2019-06-09 13:08:25 +02:00
Mickael Remond 6fc12e9779 Fix import and test 2019-06-09 13:08:25 +02:00
Mickael Remond 2d95ca9384 Simplify component writing and make it similar to client 2019-06-09 13:08:25 +02:00
Mickael Remond 736a60cd1b Use StreamClient interface in StreamManager 2019-06-09 13:08:25 +02:00
Mickael Remond 021f6d3740 Refactor ClientManager into a more generic StreamManager 2019-06-09 13:08:25 +02:00
Mickael Remond 54dfa60f12 Clean-up 2019-06-09 13:08:25 +02:00
Mickaël Rémond 36900cee20 Update README.md 2019-06-08 19:16:20 +02:00
Mickael Remond d4a8616da2 Move examples out of the cmd directory
They are now in _examples dir.
Fix #26
2019-06-08 11:34:09 +02:00
Mickael Remond b7461ae97f Do not reconnect on "connection replaced" stream errors
Fix #45
2019-06-08 11:15:51 +02:00
Mickael Remond 3689448c90 Adds an example directly in README file to get a feel of the API 2019-06-07 16:33:10 +02:00
Mickael Remond 0865f4e35c Improves comments 2019-06-07 16:30:57 +02:00
Mickael Remond eb2b506e3b Add helpers to access full / bare jid as string 2019-06-07 16:25:18 +02:00
Mickael Remond ae153e1ee5 Fix filename 2019-06-07 16:00:58 +02:00
Mickael Remond 1be04b0fba Expose JID fields and rename to match XEP-0029 wording
See: XEP-0029 - Definition of Jabber Identifiers (JIDs)
https://xmpp.org/extensions/xep-0029.html
2019-06-07 15:56:41 +02:00
Mickael Remond 269f78b30d Fix typo 2019-06-07 15:40:34 +02:00
Mickael Remond 2d8d4516fd Handling basic unrecoverable errors
Fix #43
2019-06-07 15:23:23 +02:00
Mickael Remond d45dd6a44a Returned client will be nil if parameter are incorrect 2019-06-07 12:16:58 +02:00
Mickael Remond b8fdc510a6 Further improvements on JID parsing 2019-06-07 11:40:31 +02:00
Martin/Geno 3ccc2680b0 Add typing support: XEP-0085: Chat State Notifications 2019-06-07 09:25:13 +02:00
Martin/Geno 3ea0e38f98 fix chat markers - id is a attribute not element 2019-06-07 09:24:00 +02:00
Mickael Remond b7c21871b1 Add TODO 2019-06-06 19:12:31 +02:00
Mickael Remond a451e64638 Improves comments 2019-06-06 12:01:49 +02:00
Mickaël Rémond 2f391fde80 Add Client Manager to monitor connection state and trigger reconnect (#39)
- Support for exponential backoff on reconnect to be gentle on the server.
- Clean up client by moving metrics and retry strategy to the connection manager.
- Update echo_client to use client manager
- Fix echo client XMPP message matching

Fixes #21
Improvements for #8
2019-06-06 11:58:50 +02:00
Mickael Remond 6cdadc95e9 Expose type registry for custom user-defined payload and extensions 2019-06-05 10:23:18 +02:00
Mickael Remond b93a3a2550 Improve JID parsing
Clean up tests
Fix #1
2019-06-05 10:02:24 +02:00
Mickael Remond 80d8d6d231 Apply namespace fixes from #33 2019-06-05 08:51:21 +02:00
Martin/Geno f034b74b54 fix import after moving 2019-06-05 08:41:40 +02:00
Martin/Geno e7c57cad97 complete XEP-0333 support (with displayed) 2019-06-05 08:39:35 +02:00
Mickaël Rémond 4e597505f4 Remove unused import 2019-06-04 19:38:15 +02:00
Martin/Geno 15ceab9fc4 easy xmlformat output 2019-06-04 19:11:46 +02:00
Mickael Remond 4c23014051 Test clean up 2019-06-04 19:01:19 +02:00
Mickael Remond 06ee607f53 Run tests on Golang 1.12 2019-06-04 18:59:34 +02:00
Mickael Remond 0e110bc412 Fix vanity URL import 2019-06-04 18:47:44 +02:00
Mickael Remond 57ed387f4f Add support for Out of Band markers (OOB) from XEP-0066 2019-06-04 18:47:44 +02:00
Mickael Remond 67eaed98b6 Add support for chat markers parsing (XEP-0333) 2019-06-04 18:47:44 +02:00
Mickael Remond 7a4364be95 Refactor / clean up registry 2019-06-04 18:47:44 +02:00
Mickael Remond 836e723273 Refactor / extract the registry
Work in progress
2019-06-04 18:47:44 +02:00
Mickael Remond b05efea81d Quick prototype of message extension
This is a work-in-progress, needs refactor.
2019-06-04 18:47:44 +02:00
Mickael Remond f74f276a22 Fix me note 2019-06-04 18:47:44 +02:00
Mickaël Rémond 0f6ff41792 Merge pull request #22 from genofire/fix-component
[BUGFIX] no pointer in type case in component
2019-05-31 19:44:44 +02:00
Mickael Remond b3a6429e0e Check for errors in component connect. 2019-05-31 19:41:32 +02:00
Mickael Remond e54260ec68 Clarify use of insecure flag 2019-05-31 19:22:36 +02:00
Mickael Remond 996feb1a40 Better documentation for config 2019-05-31 19:11:15 +02:00
Mickael Remond b62533d005 Fix codecov badge 2019-05-31 19:10:42 +02:00
Mickael Remond d31fc9b34c Disable Codecov comments on PR 2019-05-31 19:08:20 +02:00
Mickaël Rémond afe2017b8b Merge pull request #23 from FluuxIO/xmpp-check
Merge XMPP check branch
2019-05-31 19:02:48 +02:00
Mickaël Rémond c55257cbed Merge branch 'master' into xmpp-check 2019-05-31 19:02:10 +02:00
Mickael Remond f390433700 Add README for component. 2019-05-31 18:56:24 +02:00
Martin/Geno 757e339946 [BUGFIX] no pointer in type case in component 2019-05-31 13:46:57 +02:00
Mickaël Rémond 95dded61a1 Update README.md 2019-05-16 18:10:06 +02:00
Mickaël Rémond da0a8b9c29 Add readme for XMPP check domain 2019-05-16 18:09:39 +02:00
Mickaël Rémond 53916900d4 Update xmpp_test.go 2019-05-16 18:04:09 +02:00
Mickaël Rémond 5d6329f0b4 Update presence_test.go 2019-05-16 18:03:44 +02:00
Mickaël Rémond 23a710b36f Update message_test.go 2019-05-16 18:03:12 +02:00
Mickaël Rémond 1a6e4f266b Fix import path 2019-05-16 18:01:47 +02:00
Mickael Remond f45829916c Add tool to check XMPP certificate on starttls
Minor refactoring
2019-05-16 17:48:53 +02:00
Mickael Remond c642ad79fc More file to ignore 2019-05-16 17:48:53 +02:00
Mickael Remond d16c4cbba4 Add tool to check XMPP certificate on starttls
Minor refactoring
2019-05-16 17:46:36 +02:00
Mickael Remond 67d9170354 More file to ignore 2019-05-16 16:13:19 +02:00
Mickaël Rémond 91a7cc9c64 Merge pull request #16 from TheoMcGinley/infinite-retries-fix
Fixed infinite retries for failed TCP dial
2019-02-11 09:48:30 +01:00
Mickaël Rémond ffcde39ba6 Add test (and refactor them) for PR#15 (#18)
* Add test (and refactor them) for #15
* Update Dockerfile to support Go modules on Codeship
2019-02-10 17:53:18 +01:00
Mickaël Rémond 392d3a1ae7 Merge pull request #15 from TheoMcGinley/presence-parsing-fix
Parse show, status, and priority of presence stanzas as child elements
2019-02-10 17:17:37 +01:00
Mickaël Rémond da4ae4693e Merge pull request #17 from TheoMcGinley/close-channel-after-error
Closed receiver chan on error
2019-02-09 22:49:38 +01:00
Theo McGinley 48bc14b3e0 Closed receiver chan on error 2019-02-09 15:48:27 +00:00
Theo McGinley adf2c13a8c Fixed infinite retries for failed TCP dial 2019-02-09 14:35:31 +00:00
Theo McGinley e1cb9ac037 Parse show, status, and priority of presence stanzas as child elements instead of attributes 2019-02-09 14:18:37 +00:00
Mickael Remond c0f3d20440 Use tagged version for soundcloud and mgp123 2019-01-23 09:22:15 +01:00
Mickael Remond cf836f5f71 Add CoC and contribution guide 2019-01-21 16:24:26 +01:00
Mickaël Rémond b030e8dd4b Merge pull request #13 from hypafrag/xml_charset_reader_support
added charsets support
2019-01-17 12:03:37 +01:00
Mickaël Rémond 140d3b4d95 Merge pull request #14 from SamWhited/support_modules
Support Go Modules
2019-01-17 12:02:21 +01:00
Sam Whited fab47e1a4b Support Go Modules 2019-01-16 10:27:34 -06:00
Mickaël Rémond 94d9cbf7fa Update README.md 2019-01-10 18:12:07 +01:00
Mickael Remond 401f0be40c Detail on dependency policy. 2018-12-26 19:29:57 +01:00
Mickaël Rémond 9bebbda379 Update README.md 2018-12-26 19:24:07 +01:00
Mickael Remond a35a7959d7 Fix typo 2018-12-26 19:20:56 +01:00
Mickael Remond be587fac4c Fix path in Dockerfile 2018-12-26 19:18:42 +01:00
Mickael Remond f837b8be87 Merge branch 'master' of github.com:FluuxIO/go-xmpp 2018-12-26 19:11:02 +01:00
Mickael Remond c7cdf3b5f3 Fix broken imports 2018-12-26 19:10:30 +01:00
Mickaël Rémond 590eed1d07 More obvious link to GoDoc 2018-12-26 18:59:40 +01:00
Mickael Remond 5eae7f4ef7 Move project to gosrc.io/xmpp
The URL will be more permanent as this is a place we dedicate as short URL for our go projects.
2018-12-26 18:50:01 +01:00
hypafrag 3a51dce786 added charsets support 2018-10-13 19:45:48 +07:00
Mickael Remond 95585866c2 Add timing metrics in client.
This can be used to monitor/troubleshoot server performance.
2018-09-26 17:26:14 +02:00
Mickael Remond f70e2ca9a7 Rename options -> config 2018-09-26 16:27:37 +02:00
Mickael Remond fa5590e921 Rename Options to Config 2018-09-26 16:25:04 +02:00
Mickael Remond 1c3aaad174 Minor: comment reformat. 2018-09-23 18:43:46 +02:00
Mickael Remond a43518b976 Better style 2018-09-23 18:40:13 +02:00
Mickael Remond 07b0d2d14d Cleanup and add test for IOT control set parsing 2018-02-13 23:04:13 +01:00
Mickael Remond d6bedfb033 Minor cleanup 2018-02-13 22:07:15 +01:00
Mickael Remond 24b8d7da3d Make demo component generic 2018-01-26 12:37:27 +01:00
Mickael Remond bb1621364a Demo support for items browsing 2018-01-26 11:40:59 +01:00
Mickael Remond c451e3bc63 Add support for disco info node 2018-01-26 11:40:34 +01:00
Mickael Remond 266ed9b1e4 Do not marshal 'empty' error elements 2018-01-26 11:16:04 +01:00
Mickael Remond ad6e09a0f6 Implements send / send raw 2018-01-26 09:55:39 +01:00
Mickael Remond 2cd8eed765 Implement disco#items parsing and marshaling 2018-01-26 09:24:34 +01:00
Mickael Remond 3e6cf2c8b0 Add link to component protocol 2018-01-25 23:20:22 +01:00
Mickael Remond bdfd035bf3 Handshake minor refactor 2018-01-25 23:16:55 +01:00
Mickael Remond 4173d9ee70 Fix link format 2018-01-25 23:04:08 +01:00
Mickael Remond b9b77f6be9 Improve code documentation 2018-01-25 23:02:01 +01:00
Mickael Remond ca148e5fe5 Improve code documentation 2018-01-25 17:04:19 +01:00
Mickael Remond 7e50d313ea Improve presence / message test 2018-01-25 11:00:20 +01:00
Mickael Remond 28fb5bf61b Add basic test for messages 2018-01-24 09:38:02 +01:00
Mickael Remond 7ae2adca9f Adds support for error element on message and presence 2018-01-23 09:10:10 +01:00
Mickael Remond 8cb1e1264e Clean-up 2018-01-23 09:08:21 +01:00
Mickael Remond cb2af43fe3 Decode presence and message for components 2018-01-23 08:55:15 +01:00
Mickael Remond 57cc0a25ac Clean-up 2018-01-22 23:33:16 +01:00
Mickaël Rémond adb14260f0 Merge pull request #6 "Error parsing / generation" 2018-01-20 18:58:08 +01:00
Mickael Remond fb8d050a00 IQ error management 2018-01-20 18:56:07 +01:00
Mickael Remond 8470c01c09 Implement error parsing 2018-01-20 18:09:13 +01:00
Mickael Remond bbfafbb32c Clean-up / Consistency 2018-01-18 17:03:54 +01:00
Mickael Remond 993ca630f7 Test and code refactor 2018-01-17 18:47:34 +01:00
Mickaël Rémond 80f2888cff Update README.md 2018-01-16 22:36:15 +01:00
Mickael Remond d33490cdc0 Work-in-progress on dynamic IQ parsing 2018-01-16 22:33:21 +01:00
Mickaël Rémond 2e47f1659d Merge pull request #5 from FluuxIO/parsing
Improve IQ parsing and generation
2018-01-15 12:54:37 +01:00
Mickael Remond 20c2c44941 Fix broken tests 2018-01-15 12:52:28 +01:00
Mickael Remond b3c11fb151 If codecov script is not available, do not try to upload 2018-01-15 12:52:16 +01:00
Mickael Remond c821267928 Do not repeat xmlns in attributes on parsing 2018-01-15 12:47:26 +01:00
Mickael Remond dade3504f0 Improve generic IQ parsing 2018-01-15 12:28:34 +01:00
Mickael Remond ff2da776d3 Basic test component (disco Info) 2018-01-14 16:54:12 +01:00
Mickaël Rémond ceeb51ce0e Merge pull request #4 from FluuxIO/component
Add initial support for XMPP components
2018-01-13 19:33:45 +01:00
Mickael Remond 94815de173 Makes parsing of inner IQ XML generic 2018-01-13 19:27:46 +01:00
Mickael Remond e14f58d9a9 Decode query 2018-01-13 19:14:26 +01:00
Mickael Remond ec95020ac2 Fix failing test 2018-01-13 18:56:38 +01:00
Mickael Remond 10219ec1e6 Refactor parsing / improve typing 2018-01-13 18:50:17 +01:00
Mickael Remond 01063ec284 Refactor attributes name 2018-01-13 17:54:07 +01:00
Mickael Remond d2765aec15 Refactor namespace handling 2018-01-13 17:46:10 +01:00
Mickael Remond 24ac2c0526 Keeps component connection open 2018-01-12 19:08:47 +01:00
Mickaël Rémond fde0faca09 Add Go Report Cart badge 2018-01-12 19:03:21 +01:00
Mickael Remond b21fee420f Code clean-up 2018-01-12 18:14:41 +01:00
Mickael Remond 90865aeb8e Adhoc test component can successfully connect to ejabberd 2018-01-12 18:01:27 +01:00
Mickael Remond b31c29a03d Implements dummy auth + stream error 2018-01-11 23:00:59 +01:00
Mickael Remond ec68a04554 Component skeleton 2018-01-11 22:15:54 +01:00
Mickael Remond ce61a253af Fix formatting 2018-01-10 22:09:19 +01:00
Mickael Remond 8a611050b4 Add package level documentation. 2018-01-10 22:00:24 +01:00
Mickael Remond 75c9416763 Add badge pointing to documentation 2018-01-10 15:33:16 +01:00
Mickael Remond 5c291c13b5 Fix custom import path 2018-01-08 22:06:22 +01:00
Mickael Remond 23d91551c0 Fix custom import path 2018-01-08 14:56:03 +01:00
Mickael Remond 302e971773 Remove travis support 2018-01-05 12:50:38 +01:00
Mickael Remond 51d6759354 Workaround Codeship coverage upload report issues
Codeship / Codecov docs are incorrect, but it seems I could make it work with this workaround.
2018-01-02 16:21:45 +01:00
Mickael Remond fb5911564c Codecov requires git command 2018-01-01 19:21:11 +01:00
Mickael Remond 06cb1804a8 Use Codeship syntax example 2018-01-01 19:15:23 +01:00
Mickael Remond 76a6d35a8b Curl is already installed as default 2018-01-01 19:13:30 +01:00
Mickael Remond 4016e15a6a Install curl in build image 2018-01-01 19:08:37 +01:00
Mickael Remond d13e87f5bb Fix Codecov upload 2018-01-01 19:04:48 +01:00
Mickael Remond fef7d1ec50 Test for multiple packages 2018-01-01 19:02:30 +01:00
Mickael Remond 843059b096 Add missing codecov token 2018-01-01 18:59:19 +01:00
Mickael Remond a6b003ccd3 Add Codecov support 2018-01-01 18:57:56 +01:00
Mickael Remond 753a872fe8 Add new test badge from Codeship 2018-01-01 18:43:08 +01:00
Mickael Remond 1ea560ba1e Fix missing packages and references 2018-01-01 18:35:47 +01:00
Mickael Remond f1cda2c899 Download dependencies before build / test 2018-01-01 18:30:24 +01:00
Mickael Remond 9f0a26f9d8 Add missing Dockerfile 2018-01-01 18:28:54 +01:00
Mickael Remond 4f4a106602 Move testing to Codeship 2018-01-01 18:23:36 +01:00
Mickael Remond 710174b165 Moving XMPP library to Fluux project 2018-01-01 18:12:33 +01:00
Mickael Remond 083b9c7755 Do not expect authentication when client forbid insecure auth 2017-10-21 15:23:11 +02:00
Mickael Remond 1154df3f97 Do not use log fatal error when reading from a closed XML stream 2017-10-21 15:14:13 +02:00
Mickael Remond d5221f1a11 Add insecure option to forbid connection without TLS
(Thanks to Stevie)
2017-10-21 14:51:52 +02:00
Mickaël Rémond 683085125c Fix Coveralls badge 2017-10-21 14:08:44 +02:00
Mickael Remond e51fffcaed Fix typo 2017-10-21 13:58:58 +02:00
Mickael Remond 4ac645a9ec Code cleanup 2017-10-20 16:53:15 +02:00
Mickael Remond 228cb14491 Test refactoring 2017-10-05 20:11:28 -04:00
Mickael Remond 2579c84481 Initial basic XMPP server mock.
Work in progress: Need refactoring.
2017-10-04 19:27:35 -04:00
Mickael Remond b186813e91 Comment broken test 2017-10-04 16:51:28 -04:00
Mickael Remond 62fc9a407e Expand test 2017-10-04 16:42:00 -04:00
Mickael Remond cb388cd89e Add TODO comment 2017-10-04 16:39:34 -04:00
Mickael Remond d4f193a1bd Ignore MacOS Finder files 2017-10-04 16:27:18 -04:00
Mickael Remond c41ed1c32f Add XMPP Jukebox demo 2016-03-14 11:31:31 +01:00
Mickael Remond d26d066540 Basic formatting of user tune publication 2016-02-17 15:35:24 +01:00
Mickael Remond a2aab652a9 Support connection timeout and retries 2016-02-17 13:45:39 +01:00
Mickael Remond 82c01de54b Export presence packet 2016-02-17 11:28:51 +01:00
Mickael Remond 06beebc812 Fix Coveralls badge image 2016-02-15 18:40:48 +01:00
Mickael Remond 09bf85b1e8 Add Travis and Coveralls badges 2016-02-15 18:39:12 +01:00
Mickael Remond e6a645dee0 Fix missing type on IQ 2016-02-15 18:33:51 +01:00
Mickael Remond 6fdef748be Git should ignore build result 2016-02-15 16:45:51 +01:00
Mickael Remond 57835bfcb5 Allow formatting IOT Control SetResponse 2016-02-15 16:33:27 +01:00
Mickael Remond 268acbff07 Refactor IQ handling and merge payload iq struct fields for supported XEPs 2016-02-15 15:22:51 +01:00
Mickael Remond 3a516a43d3 Add preliminary support for IoT control (XEP-0325) 2016-02-15 11:08:54 +01:00
Mickael Remond adcd2bd467 Add Documentation 2016-02-15 11:05:44 +01:00
Mickael Remond dc30c70c17 Reformat error 2016-02-15 11:04:42 +01:00
Mickael Remond 4d3463458e Fix / improve code comments 2016-02-13 17:01:06 +01:00
Mickaël Rémond e29241b09d Update LICENSE 2016-01-06 19:31:28 +01:00
Mickaël Rémond 14666841aa Make our libraries license use Apache v2
More homogeneous with our other libraries.
2016-01-06 19:30:53 +01:00
Mickael Remond 5949967daf Add more test on jid parsing 2016-01-06 19:21:37 +01:00
Mickael Remond f7651c7785 Fix Coveralls support 2016-01-06 18:31:40 +01:00
Mickael Remond 190b1b53a6 Fix Coveralls support 2016-01-06 18:29:11 +01:00
Mickael Remond ac881fa6a4 Use faster Travis test infra 2016-01-06 18:24:16 +01:00
Mickael Remond 3ca015f307 Fix import after project move to processone github 2016-01-06 18:22:30 +01:00
Mickael Remond 30312aa82a Ignore coverage result file 2016-01-06 18:21:47 +01:00
Mickael Remond 05297ce475 Configure Coveralls 2016-01-06 18:06:30 +01:00
Mickael Remond c392810d29 Adding basic tests 2016-01-06 17:59:44 +01:00
Mickael Remond 1b90d5d2ef Bugfix: Do not reopen stream if starttls was not enabled. 2016-01-06 17:19:16 +01:00
Mickael Remond ff334ba729 Move example client in its own cmd directory 2016-01-06 17:17:48 +01:00
Mickael Remond 2da7c27d18 Add TODO comment for later fix 2016-01-06 17:08:51 +01:00
Mickael Remond c5732bbf1a Initial working version of go XMPP library 2016-01-06 16:51:12 +01:00
Mickael Remond f237b861bb Describe goal of the project. 2015-12-29 11:52:02 +01:00
Mickael Remond d8c8419cb1 Describe goal of the project. 2015-12-29 11:49:43 +01:00
Mickael Remond 6f15887129 Add license and update .gitignore 2015-12-29 09:38:46 +01:00
Mickaël Rémond c9340b668d Initial commit 2015-12-29 09:35:46 +01:00
115 changed files with 7435 additions and 3037 deletions
Executable
+36
View File
@@ -0,0 +1,36 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
coverage.out
coverage.txt
.idea/
*.iml
.DS_Store
# Do not commit codeship key
codeship.aes
codeship.env
priv/
-5
View File
@@ -1,5 +0,0 @@
language: go
go:
- tip
script:
- go test
+76
View File
@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at contact@process-one.net. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+139
View File
@@ -0,0 +1,139 @@
# Contributing
We'd love for you to contribute to our source code and to make our project even better than it is
today! Here are the guidelines we'd like you to follow:
* [Code of Conduct](#coc)
* [Questions and Problems](#question)
* [Issues and Bugs](#issue)
* [Feature Requests](#feature)
* [Issue Submission Guidelines](#submit)
* [Pull Request Submission Guidelines](#submit-pr)
* [Signing the CLA](#cla)
## <a name="coc"></a> Code of Conduct
Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc].
## <a name="requests"></a> Questions, Bugs, Features
### <a name="question"></a> Got a Question or Problem?
Do not open issues for general support questions as we want to keep GitHub issues for bug reports
and feature requests. You've got much better chances of getting your question answered on dedicated
support platforms, the best being [Stack Overflow][stackoverflow].
Stack Overflow is a much better place to ask questions since:
- there are thousands of people willing to help on Stack Overflow
- questions and answers stay available for public viewing so your question / answer might help
someone else
- Stack Overflow's voting system assures that the best answers are prominently visible.
To save your and our time, we will systematically close all issues that are requests for general
support and redirect people to the section you are reading right now.
### <a name="issue"></a> Found an Issue or Bug?
If you find a bug in the source code, you can help us by submitting an issue to our
[GitHub Repository][github]. Even better, you can submit a Pull Request with a fix.
### <a name="feature"></a> Missing a Feature?
You can request a new feature by submitting an issue to our [GitHub Repository][github-issues].
If you would like to implement a new feature then consider what kind of change it is:
* **Major Changes** that you wish to contribute to the project should be discussed first in an
[GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature.
* **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github]
as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr).
## <a name="submit"></a> Issue Submission Guidelines
Before you submit your issue search the archive, maybe your question was already answered.
If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize
the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to
make it easier to understand and categorize the issue.
## <a name="submit-pr"></a> Pull Request Submission Guidelines
By submitting a pull request for a code or doc contribution, you need to have the right
to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla]
for details.
Before you submit your pull request consider the following guidelines:
* Search [GitHub][github-pr] for an open or closed Pull Request
that relates to your submission. You don't want to duplicate effort.
* Make your changes in a new git branch:
```shell
git checkout -b my-fix-branch master
```
* Test your changes and, if relevant, expand the automated test suite.
* Create your patch commit, including appropriate test cases.
* If the changes affect public APIs, change or add relevant documentation.
* Commit your changes using a descriptive commit message.
```shell
git commit -a
```
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
* Push your branch to GitHub:
```shell
git push origin my-fix-branch
```
* In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test.
We will also notify you if you have not yet signed the [contribution agreement][cla].
* If you find that the continunous integration has failed, look into the logs to find out
if your changes caused test failures, the commit message was malformed etc. If you find that the
tests failed or times out for unrelated reasons, you can ping a team member so that the build can be
restarted.
* If we suggest changes, then:
* Make the required updates.
* Test your changes and test cases.
* Commit your changes to your branch (e.g. `my-fix-branch`).
* Push the changes to your GitHub repository (this will update your Pull Request).
You can also amend the initial commits and force push them to the branch.
```shell
git rebase master -i
git push origin my-fix-branch -f
```
This is generally easier to follow, but separate commits are useful if the Pull Request contains
iterations that might be interesting to see side-by-side.
That's it! Thank you for your contribution!
## <a name="cla"></a> Signing the Contributor License Agreement (CLA)
Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done
so before. It's a quick process, we promise, and you will be able to do it all online
You can read [ProcessOne Contribution License Agreement][cla] in PDF.
This is part of the legal framework of the open-source ecosystem that adds some red tape,
but protects both the contributor and the company / foundation behind the project. It also
gives us the option to relicense the code with a more permissive license in the future.
[coc]: https://github.com/FluuxIO/go-xmpp/blob/master/CODE_OF_CONDUCT.md
[stackoverflow]: https://stackoverflow.com/
[github]: https://github.com/FluuxIO/go-xmpp
[github-issues]: https://github.com/FluuxIO/go-xmpp/issues
[github-new-issue]: https://github.com/FluuxIO/go-xmpp/issues/new
[github-pr]: https://github.com/FluuxIO/go-xmpp/pulls
[cla]: https://www.process-one.net/resources/ejabberd-cla.pdf
[license]: https://github.com/FluuxIO/go-xmpp/blob/master/LICENSE
+4
View File
@@ -0,0 +1,4 @@
FROM golang:1.12
WORKDIR /xmpp
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
COPY . ./
+22 -20
View File
@@ -1,27 +1,29 @@
Copyright (c) 2009 The Go Authors. All rights reserved. BSD 3-Clause License
Copyright (c) 2017, ProcessOne SARL
All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are modification, are permitted provided that the following conditions are met:
met:
* Redistributions of source code must retain the above copyright * Redistributions of source code must retain the above copyright notice, this
notice, this list of conditions and the following disclaimer. list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer * Redistributions in binary form must reproduce the above copyright notice,
in the documentation and/or other materials provided with the this list of conditions and the following disclaimer in the documentation
distribution. and/or other materials provided with the distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from contributors may be used to endorse or promote products derived from
this software without specific prior written permission. this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+131 -4
View File
@@ -1,6 +1,133 @@
go-xmpp # Fluux XMPP
=======
go xmpp library (original was written by russ cox ) [![Codeship Status for FluuxIO/xmpp](https://app.codeship.com/projects/dba7f300-d145-0135-6c51-26e28af241d2/status?branch=master)](https://app.codeship.com/projects/262399) [![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![codecov](https://codecov.io/gh/FluuxIO/go-xmpp/branch/master/graph/badge.svg)](https://codecov.io/gh/FluuxIO/go-xmpp)
[Documentation](https://godoc.org/github.com/xmppo/go-xmpp) Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
The goal is to make simple to write simple XMPP clients and components:
- For automation (like for example monitoring of an XMPP service),
- For building connected "things" by plugging them on an XMPP server,
- For writing simple chatbot to control a service or a thing,
- For writing XMPP servers components.
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
## Configuration and connection
### Allowing Insecure TLS connection during development
It is not recommended to disable the check for domain name and certificate chain. Doing so would open your client
to man-in-the-middle attacks.
However, in development, XMPP servers often use self-signed certificates. In that situation, it is better to add the
root CA that signed the certificate to your trusted list of root CA. It avoids changing the code and limit the risk
of shipping an insecure client to production.
That said, if you really want to allow your client to trust any TLS certificate, you can customize Go standard
`tls.Config` and set it in Config struct.
Here is an example code to configure a client to allow connecting to a server with self-signed certificate. Note the
`InsecureSkipVerify` option. When using this `tls.Config` option, all the checks on the certificate are skipped.
```go
config := xmpp.Config{
Address: "localhost:5222",
Jid: "test@localhost",
Password: "test",
TLSConfig: tls.Config{InsecureSkipVerify: true},
}
```
## Supported specifications
### Clients
- [RFC 6120: XMPP Core](https://xmpp.org/rfcs/rfc6120.html)
- [RFC 6121: XMPP Instant Messaging and Presence](https://xmpp.org/rfcs/rfc6121.html)
### Components
- [XEP-0114: Jabber Component Protocol](https://xmpp.org/extensions/xep-0114.html)
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
## Stanza subpackage
XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to
leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas
back and forth.
At a low-level, stanzas are XML fragments. However, Fluux XMPP library provides the building blocks to interact with
stanzas at a high-level, providing a Go-friendly API.
The `stanza` subpackage provides support for XMPP stream parsing, marshalling and unmarshalling of XMPP stanza. It is a
bridge between high-level Go structure and low-level XMPP protocol.
Parsing, marshalling and unmarshalling is automatically handled by Fluux XMPP client library. As a developer, you will
generally manipulates only the high-level structs provided by the stanza package.
The XMPP protocol, as the name implies is extensible. If your application is using custom stanza extensions, you can
implement your own extensions directly in your own application.
To learn more about the stanza package, you can read more in the
[stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md).
## Examples
We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using
Fluux XMPP library.
Here is the demo "echo" client:
```go
package main
import (
"fmt"
"log"
"os"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
config := xmpp.Config{
Address: "localhost:5222",
Jid: "test@localhost",
Password: "test",
StreamLogger: os.Stdout,
Insecure: true,
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
client, err := xmpp.NewClient(config, router)
if err != nil {
log.Fatalf("%+v", err)
}
// If you pass the client to a connection manager, it will handle the reconnect policy
// for you automatically.
cm := xmpp.NewStreamManager(client, nil)
log.Fatal(cm.Run())
}
func handleMessage(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
return
}
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}
```
## Reference documentation
The code documentation is available on GoDoc: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
-113
View File
@@ -1,113 +0,0 @@
package main
import (
"crypto/tls"
"log"
"os"
"strings"
"github.com/matterbridge/go-xmpp"
"github.com/mattn/go-gtk/gtk"
)
func main() {
gtk.Init(&os.Args)
window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL)
window.SetTitle("GoTalk")
window.Connect("destroy", func() {
gtk.MainQuit()
})
vbox := gtk.NewVBox(false, 1)
scrolledwin := gtk.NewScrolledWindow(nil, nil)
textview := gtk.NewTextView()
textview.SetEditable(false)
textview.SetCursorVisible(false)
scrolledwin.Add(textview)
vbox.Add(scrolledwin)
buffer := textview.GetBuffer()
entry := gtk.NewEntry()
vbox.PackEnd(entry, false, false, 0)
window.Add(vbox)
window.SetSizeRequest(300, 400)
window.ShowAll()
dialog := gtk.NewDialog()
dialog.SetTitle(window.GetTitle())
sgroup := gtk.NewSizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
hbox := gtk.NewHBox(false, 1)
dialog.GetVBox().Add(hbox)
label := gtk.NewLabel("username:")
sgroup.AddWidget(label)
hbox.Add(label)
username := gtk.NewEntry()
hbox.Add(username)
hbox = gtk.NewHBox(false, 1)
dialog.GetVBox().Add(hbox)
label = gtk.NewLabel("password:")
sgroup.AddWidget(label)
hbox.Add(label)
password := gtk.NewEntry()
password.SetVisibility(false)
hbox.Add(password)
dialog.AddButton(gtk.STOCK_OK, gtk.RESPONSE_OK)
dialog.AddButton(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
dialog.SetDefaultResponse(gtk.RESPONSE_OK)
dialog.SetTransientFor(window)
dialog.ShowAll()
res := dialog.Run()
username_ := username.GetText()
password_ := password.GetText()
dialog.Destroy()
if res != gtk.RESPONSE_OK {
os.Exit(0)
}
xmpp.DefaultConfig = tls.Config{
ServerName: "talk.google.com",
InsecureSkipVerify: false,
}
talk, err := xmpp.NewClient("talk.google.com:443", username_, password_, false)
if err != nil {
log.Fatal(err)
}
entry.Connect("activate", func() {
text := entry.GetText()
tokens := strings.SplitN(text, " ", 2)
if len(tokens) == 2 {
func() {
defer recover()
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
entry.SetText("")
}()
}
})
go func() {
for {
func() {
defer recover()
chat, err := talk.Recv()
if err != nil {
log.Fatal(err)
}
var iter gtk.TextIter
buffer.GetStartIter(&iter)
if msg, ok := chat.(xmpp.Chat); ok {
buffer.Insert(&iter, msg.Remote+": "+msg.Text+"\n")
}
}()
}
}()
gtk.Main()
}
-97
View File
@@ -1,97 +0,0 @@
package main
import (
"bufio"
"crypto/tls"
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/matterbridge/go-xmpp"
)
var (
server = flag.String("server", "talk.google.com:443", "server")
username = flag.String("username", "", "username")
password = flag.String("password", "", "password")
status = flag.String("status", "xa", "status")
statusMessage = flag.String("status-msg", "I for one welcome our new codebot overlords.", "status message")
notls = flag.Bool("notls", false, "No TLS")
debug = flag.Bool("debug", false, "debug output")
session = flag.Bool("session", false, "use server session")
)
func serverName(host string) string {
return strings.Split(host, ":")[0]
}
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: example [options]\n")
flag.PrintDefaults()
os.Exit(2)
}
flag.Parse()
if *username == "" || *password == "" {
if *debug && *username == "" && *password == "" {
fmt.Fprintf(os.Stderr, "no username or password were given; attempting ANONYMOUS auth\n")
} else if *username != "" || *password != "" {
flag.Usage()
}
}
if !*notls {
xmpp.DefaultConfig = tls.Config{
ServerName: serverName(*server),
InsecureSkipVerify: false,
}
}
var talk *xmpp.Client
var err error
options := xmpp.Options{
Host: *server,
User: *username,
Password: *password,
NoTLS: *notls,
Debug: *debug,
Session: *session,
Status: *status,
StatusMessage: *statusMessage,
}
talk, err = options.NewClient()
if err != nil {
log.Fatal(err)
}
go func() {
for {
chat, err := talk.Recv()
if err != nil {
log.Fatal(err)
}
switch v := chat.(type) {
case xmpp.Chat:
fmt.Println(v.Remote, v.Text)
case xmpp.Presence:
fmt.Println(v.From, v.Show)
}
}
}()
for {
in := bufio.NewReader(os.Stdin)
line, err := in.ReadString('\n')
if err != nil {
continue
}
line = strings.TrimRight(line, "\n")
tokens := strings.SplitN(line, " ", 2)
if len(tokens) == 2 {
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
}
}
}
+5
View File
@@ -0,0 +1,5 @@
# Custom Stanza example
This module show how to implement a custom extension for your own client, without having to modify or fork Fluux XMPP.
It help integrating your custom extension in the standard stream parsing, marshalling and unmarshalling workflow.
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"encoding/xml"
"fmt"
"log"
"gosrc.io/xmpp/stanza"
)
func main() {
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"})
payload := CustomPayload{XMLName: xml.Name{Space: "my:custom:payload", Local: "query"}, Node: "test"}
iq.Payload = payload
data, err := xml.Marshal(iq)
if err != nil {
log.Fatalf("Cannot marshal iq with custom payload: %s", err)
}
var parsedIQ stanza.IQ
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
log.Fatalf("Cannot unmarshal(%s): %s", data, err)
}
parsedPayload, ok := parsedIQ.Payload.(*CustomPayload)
if !ok {
log.Fatalf("Incorrect payload type: %#v", parsedIQ.Payload)
}
fmt.Printf("Parsed Payload: %#v", parsedPayload)
if parsedPayload.Node != "test" {
log.Fatalf("Incorrect node value: %s", parsedPayload.Node)
}
}
type CustomPayload struct {
XMLName xml.Name `xml:"my:custom:payload query"`
Node string `xml:"node,attr,omitempty"`
}
func (c CustomPayload) Namespace() string {
return c.XMLName.Space
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
}
+5
View File
@@ -0,0 +1,5 @@
# Advanced component: delegation
`delegation` is an example of advanced component supporting Namespace Delegation
([XEP-0355](https://xmpp.org/extensions/xep-0355.html)) and privileged entity
([XEP-356](https://xmpp.org/extensions/xep-0356.html)).
+206
View File
@@ -0,0 +1,206 @@
package main
import (
"encoding/xml"
"fmt"
"log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
opts := xmpp.ComponentOptions{
Domain: "service.localhost",
Secret: "mypass",
Address: "localhost:9999",
// TODO: Move that part to a component discovery handler
Name: "Test Component",
Category: "gateway",
Type: "service",
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
router.NewRoute().
IQNamespaces(stanza.NSDiscoInfo).
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
discoInfo(s, p, opts)
})
router.NewRoute().
IQNamespaces("urn:xmpp:delegation:1").
HandlerFunc(handleDelegation)
component, err := xmpp.NewComponent(opts, router)
if err != nil {
log.Fatalf("%+v", err)
}
// If you pass the component to a stream manager, it will handle the reconnect policy
// for you automatically.
// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
cm := xmpp.NewStreamManager(component, nil)
log.Fatal(cm.Run())
}
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
return
}
var msgProcessed bool
for _, ext := range msg.Extensions {
delegation, ok := ext.(*stanza.Delegation)
if ok {
msgProcessed = true
fmt.Printf("Delegation confirmed for namespace %s\n", delegation.Delegated.Namespace)
}
}
// TODO: Decode privilege message
// <message to='service.localhost' from='localhost'><privilege xmlns='urn:xmpp:privilege:1'><perm type='outgoing' access='message'/><perm type='get' access='roster'/><perm type='managed_entity' access='presence'/></privilege></message>
if !msgProcessed {
fmt.Printf("Ignored received message, not related to delegation: %v\n", msg)
}
}
const (
pubsubNode = "urn:xmpp:delegation:1::http://jabber.org/protocol/pubsub"
pepNode = "urn:xmpp:delegation:1:bare:http://jabber.org/protocol/pubsub"
)
// TODO: replace xmpp.Sender by ctx xmpp.Context ?
// ctx.Stream.Send / SendRaw
// ctx.Opts
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
// Type conversion & sanity checks
iq, ok := p.(stanza.IQ)
if !ok {
return
}
info, ok := iq.Payload.(*stanza.DiscoInfo)
if !ok {
return
}
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
switch info.Node {
case "":
discoInfoRoot(&iqResp, opts)
case pubsubNode:
discoInfoPubSub(&iqResp)
case pepNode:
discoInfoPEP(&iqResp)
}
_ = c.Send(iqResp)
}
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
disco := iqResp.DiscoInfo()
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
}
func discoInfoPubSub(iqResp *stanza.IQ) {
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Node: pubsubNode,
Features: []stanza.Feature{
{Var: "http://jabber.org/protocol/pubsub"},
{Var: "http://jabber.org/protocol/pubsub#publish"},
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
{Var: "http://jabber.org/protocol/pubsub#publish-options"},
},
}
iqResp.Payload = &payload
}
func discoInfoPEP(iqResp *stanza.IQ) {
identity := stanza.Identity{
Category: "pubsub",
Type: "pep",
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Node: pepNode,
Features: []stanza.Feature{
{Var: "http://jabber.org/protocol/pubsub#access-presence"},
{Var: "http://jabber.org/protocol/pubsub#auto-create"},
{Var: "http://jabber.org/protocol/pubsub#auto-subscribe"},
{Var: "http://jabber.org/protocol/pubsub#config-node"},
{Var: "http://jabber.org/protocol/pubsub#create-and-configure"},
{Var: "http://jabber.org/protocol/pubsub#create-nodes"},
{Var: "http://jabber.org/protocol/pubsub#filtered-notifications"},
{Var: "http://jabber.org/protocol/pubsub#persistent-items"},
{Var: "http://jabber.org/protocol/pubsub#publish"},
{Var: "http://jabber.org/protocol/pubsub#retrieve-items"},
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
},
}
iqResp.Payload = &payload
}
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
// Type conversion & sanity checks
iq, ok := p.(stanza.IQ)
if !ok {
return
}
delegation, ok := iq.Payload.(*stanza.Delegation)
if !ok {
return
}
forwardedPacket := delegation.Forwarded.Stanza
fmt.Println(forwardedPacket)
forwardedIQ, ok := forwardedPacket.(stanza.IQ)
if !ok {
return
}
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub)
if !ok {
// We only support pubsub delegation
return
}
if pubsub.Publish.XMLName.Local == "publish" {
// Prepare pubsub IQ reply
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
payload := stanza.PubSub{
XMLName: xml.Name{
Space: "http://jabber.org/protocol/pubsub",
Local: "pubsub",
},
}
iqResp.Payload = &payload
// Wrap the reply in delegation 'forward'
iqForward := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
delegPayload := stanza.Delegation{
XMLName: xml.Name{
Space: "urn:xmpp:delegation:1",
Local: "delegation",
},
Forwarded: &stanza.Forwarded{
XMLName: xml.Name{
Space: "urn:xmpp:forward:0",
Local: "forward",
},
Stanza: iqResp,
},
}
iqForward.Payload = &delegPayload
_ = s.Send(iqForward)
// TODO: The component should actually broadcast the mood to subscribers
}
}
+11
View File
@@ -0,0 +1,11 @@
module gosrc.io/xmpp/_examples
go 1.12
require (
github.com/processone/mpg123 v1.0.0
github.com/processone/soundcloud v1.0.0
gosrc.io/xmpp v0.1.1
)
replace gosrc.io/xmpp => ./../
+6
View File
@@ -0,0 +1,6 @@
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=
+22
View File
@@ -0,0 +1,22 @@
# xmpp_component
This component will connect to ejabberd and act as a subdomain "service" of your primary XMPP domain
(in that case localhost).
This component does nothing expect connect and show up in service discovery.
To be able to connect this component, you need to add a listener to your XMPP server.
Here is an example ejabberd configuration for that component listener:
```yaml
listen:
...
-
port: 8888
module: ejabberd_service
password: "mypass"
```
ejabberd will listen for a component (service) on port 8888 and allows it to connect using the
secret "mypass".
+101
View File
@@ -0,0 +1,101 @@
package main
import (
"fmt"
"log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
opts := xmpp.ComponentOptions{
Domain: "service2.localhost",
Secret: "mypass",
Address: "localhost:8888",
Name: "Test Component",
Category: "gateway",
Type: "service",
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
router.NewRoute().
IQNamespaces(stanza.NSDiscoInfo).
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
discoInfo(s, p, opts)
})
router.NewRoute().
IQNamespaces(stanza.NSDiscoItems).
HandlerFunc(discoItems)
router.NewRoute().
IQNamespaces("jabber:iq:version").
HandlerFunc(handleVersion)
component, err := xmpp.NewComponent(opts, router)
if err != nil {
log.Fatalf("%+v", err)
}
// If you pass the component to a stream manager, it will handle the reconnect policy
// for you automatically.
// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
cm := xmpp.NewStreamManager(component, nil)
log.Fatal(cm.Run())
}
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
return
}
fmt.Println("Received message:", msg.Body)
}
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
// Type conversion & sanity checks
iq, ok := p.(stanza.IQ)
if !ok || iq.Type != "get" {
return
}
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
disco := iqResp.DiscoInfo()
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
_ = c.Send(iqResp)
}
// TODO: Handle iq error responses
func discoItems(c xmpp.Sender, p stanza.Packet) {
// Type conversion & sanity checks
iq, ok := p.(stanza.IQ)
if !ok || iq.Type != "get" {
return
}
discoItems, ok := iq.Payload.(*stanza.DiscoItems)
if !ok {
return
}
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
items := iqResp.DiscoItems()
if discoItems.Node == "" {
items.AddItem("service.localhost", "node1", "test node")
}
_ = c.Send(iqResp)
}
func handleVersion(c xmpp.Sender, p stanza.Packet) {
// Type conversion & sanity checks
iq, ok := p.(stanza.IQ)
if !ok {
return
}
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
_ = c.Send(iqResp)
}
+53
View File
@@ -0,0 +1,53 @@
/*
xmpp_echo is a demo client that connect on an XMPP server and echo message received back to original sender.
*/
package main
import (
"fmt"
"log"
"os"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
config := xmpp.Config{
Address: "localhost:5222",
Jid: "test@localhost",
Password: "test",
StreamLogger: os.Stdout,
Insecure: true,
// TLSConfig: tls.Config{InsecureSkipVerify: true},
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
client, err := xmpp.NewClient(config, router)
if err != nil {
log.Fatalf("%+v", err)
}
// If you pass the client to a connection manager, it will handle the reconnect policy
// for you automatically.
cm := xmpp.NewStreamManager(client, nil)
log.Fatal(cm.Run())
}
func handleMessage(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
return
}
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
// (using templates ?)
+125
View File
@@ -0,0 +1,125 @@
// Can be launched with:
// ./xmpp_jukebox -jid=test@localhost/jukebox -password=test -address=localhost:5222
package main
import (
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/processone/mpg123"
"github.com/processone/soundcloud"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
// Get the actual song Stream URL from SoundCloud website song URL and play it with mpg123 player.
const scClientID = "dde6a0075614ac4f3bea423863076b22"
func main() {
jid := flag.String("jid", "", "jukebok XMPP JID, resource is optional")
password := flag.String("password", "", "XMPP account password")
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
flag.Parse()
// 1. Create mpg player
player, err := mpg123.NewPlayer()
if err != nil {
log.Fatal(err)
}
// 2. Prepare XMPP client
config := xmpp.Config{
Address: *address,
Jid: *jid,
Password: *password,
// StreamLogger: os.Stdout,
Insecure: true,
}
router := xmpp.NewRouter()
router.NewRoute().
Packet("message").
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
handleMessage(s, p, player)
})
router.NewRoute().
Packet("message").
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
handleIQ(s, p, player)
})
client, err := xmpp.NewClient(config, router)
if err != nil {
log.Fatalf("%+v", err)
}
cm := xmpp.NewStreamManager(client, nil)
log.Fatal(cm.Run())
}
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
msg, ok := p.(stanza.Message)
if !ok {
return
}
command := strings.Trim(msg.Body, " ")
if command == "stop" {
player.Stop()
} else {
playSCURL(player, command)
sendUserTune(s, "Radiohead", "Spectre")
}
}
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
iq, ok := p.(stanza.IQ)
if !ok {
return
}
switch payload := iq.Payload.(type) {
// We support IOT Control IQ
case *stanza.ControlSet:
var url string
for _, element := range payload.Fields {
if element.XMLName.Local == "string" && element.Name == "url" {
url = strings.Trim(element.Value, " ")
break
}
}
playSCURL(player, url)
setResponse := new(stanza.ControlSetResponse)
// FIXME: Broken
reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
_ = s.Send(reply)
// TODO add Soundclound artist / title retrieval
sendUserTune(s, "Radiohead", "Spectre")
default:
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", iq.Payload)
}
}
func sendUserTune(s xmpp.Sender, artist string, title string) {
tune := stanza.Tune{Artist: artist, Title: title}
iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
iq.Payload = &payload
_ = s.Send(iq)
}
func playSCURL(p *mpg123.Player, rawURL string) {
songID, _ := soundcloud.GetSongID(rawURL)
// TODO: Maybe we need to check the track itself to get the stream URL from reply ?
url := soundcloud.FormatStreamURL(songID)
_ = p.Play(url)
}
// TODO
// - Have a player API to play, play next, or add to queue
// - Have the ability to parse custom packet to play sound
// - Use PEP to display tunes status
// - Ability to "speak" messages
+53
View File
@@ -0,0 +1,53 @@
package xmpp
import (
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"gosrc.io/xmpp/stanza"
)
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
// TODO: Implement other type of SASL Authentication
havePlain := false
for _, m := range f.Mechanisms.Mechanism {
if m == "PLAIN" {
havePlain = true
break
}
}
if !havePlain {
err := fmt.Errorf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism)
return NewConnError(err, true)
}
return authPlain(socket, decoder, user, password)
}
// Plain authentication: send base64-encoded \x00 user \x00 password
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
raw := "\x00" + user + "\x00" + password
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
// Next message should be either success or failure.
val, err := stanza.NextPacket(decoder)
if err != nil {
return err
}
switch v := val.(type) {
case stanza.SASLSuccess:
case stanza.SASLFailure:
// v.Any is type of sub-element in failure, which gives a description of what failed.
err := errors.New("auth failure: " + v.Any.Local)
return NewConnError(err, true)
default:
return errors.New("expected SASL success or failure, got " + v.Name())
}
return err
}
+101
View File
@@ -0,0 +1,101 @@
/*
Interesting reference on backoff:
- Exponential Backoff And Jitter (AWS Blog):
https://www.awsarchitectureblog.com/2015/03/backoff.html
We use Jitter as a default for exponential backoff, as the goal of
this module is not to provide precise 'ticks', but good behaviour to
implement retries that are helping the server to recover faster in
case of congestion.
It can be used in several ways:
- Using duration to get next sleep time.
- Using ticker channel to trigger callback function on tick
The functions for Backoff are not threadsafe, but you can:
- Keep the attempt counter on your end and use durationForAttempt(int)
- Use lock in your own code to protect the Backoff structure.
TODO: Implement Backoff Ticker channel
TODO: Implement throttler interface. Throttler could be used to implement various reconnect strategies.
*/
package xmpp
import (
"math"
"math/rand"
"time"
)
const (
defaultBase int = 20 // Backoff base, in ms
defaultFactor int = 2
defaultCap int = 180000 // 3 minutes
)
// backoff provides increasing duration with the number of attempt
// performed. The structure is used to support exponential backoff on
// connection attempts to avoid hammering the server we are connecting
// to.
type backoff struct {
NoJitter bool
Base int
Factor int
Cap int
lastDuration int
attempt int
}
// duration returns the duration to apply to the current attempt.
func (b *backoff) duration() time.Duration {
d := b.durationForAttempt(b.attempt)
b.attempt++
return d
}
// wait sleeps for backoff duration for current attempt.
func (b *backoff) wait() {
time.Sleep(b.duration())
}
// durationForAttempt returns a duration for an attempt number, in a stateless way.
func (b *backoff) durationForAttempt(attempt int) time.Duration {
b.setDefault()
expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt)))
d := int(math.Trunc(expBackoff))
if !b.NoJitter {
d = rand.Intn(d)
}
return time.Duration(d) * time.Millisecond
}
// reset sets back the number of attempts to 0. This is to be called after a successful operation has been performed,
// to reset the exponential backoff interval.
func (b *backoff) reset() {
b.attempt = 0
}
func (b *backoff) setDefault() {
if b.Base == 0 {
b.Base = defaultBase
}
if b.Cap == 0 {
b.Cap = defaultCap
}
if b.Factor == 0 {
b.Factor = defaultFactor
}
}
/*
We use full jitter as default for now as it seems to provide good behaviour for reconnect.
Base is the default interval between attempts (if backoff Factor was equal to 1)
Attempt is the number of retry for operation. If we start attempt at 0, first sleep equals base.
Cap is the maximum sleep time duration we tolerate between attempts
*/
+22
View File
@@ -0,0 +1,22 @@
package xmpp
import (
"testing"
"time"
)
func TestDurationForAttempt_NoJitter(t *testing.T) {
b := backoff{Base: 25, NoJitter: true}
bInMS := time.Duration(b.Base) * time.Millisecond
if b.durationForAttempt(0) != bInMS {
t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.durationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond)
}
var prevDuration, d time.Duration
for i := 0; i < 10; i++ {
d = b.durationForAttempt(i)
if !(d >= prevDuration) {
t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration)
}
prevDuration = d
}
}
+148
View File
@@ -0,0 +1,148 @@
package xmpp
import (
"crypto/tls"
"encoding/xml"
"errors"
"fmt"
"net"
"strings"
"time"
"gosrc.io/xmpp/stanza"
)
// TODO: Should I move this as an extension of the client?
// I should probably make the code more modular, but keep concern separated to keep it simple.
type ServerCheck struct {
address string
domain string
}
func NewChecker(address, domain string) (*ServerCheck, error) {
client := ServerCheck{}
var err error
var host string
if client.address, host, err = extractParams(address); err != nil {
return &client, err
}
if domain != "" {
client.domain = domain
} else {
client.domain = host
}
return &client, nil
}
// Check triggers actual TCP connection, based on previously defined parameters.
func (c *ServerCheck) Check() error {
var tcpconn net.Conn
var err error
timeout := 15 * time.Second
tcpconn, err = net.DialTimeout("tcp", c.address, timeout)
if err != nil {
return err
}
decoder := xml.NewDecoder(tcpconn)
// Send stream open tag
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); err != nil {
return err
}
// Set xml decoder and extract streamID from reply (not used for now)
_, err = stanza.InitStream(decoder)
if err != nil {
return err
}
// extract stream features
var f stanza.StreamFeatures
packet, err := stanza.NextPacket(decoder)
if err != nil {
err = fmt.Errorf("stream open decode features: %s", err)
return err
}
switch p := packet.(type) {
case stanza.StreamFeatures:
f = p
case stanza.StreamError:
return errors.New("open stream error: " + p.Error.Local)
default:
return errors.New("expected packet received while expecting features, got " + p.Name())
}
if _, ok := f.DoesStartTLS(); ok {
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
var k stanza.TLSProceed
if err = decoder.DecodeElement(&k, nil); err != nil {
return fmt.Errorf("expecting starttls proceed: %s", err)
}
var tlsConfig tls.Config
tlsConfig.ServerName = c.domain
tlsConn := tls.Client(tcpconn, &tlsConfig)
// We convert existing connection to TLS
if err = tlsConn.Handshake(); err != nil {
return err
}
// We check that cert matches hostname
if err = tlsConn.VerifyHostname(c.domain); err != nil {
return err
}
if err = checkExpiration(tlsConn); err != nil {
return err
}
return nil
}
return errors.New("TLS not supported on server")
}
// Check expiration date for the whole certificate chain and returns an error
// if the expiration date is in less than 48 hours.
func checkExpiration(tlsConn *tls.Conn) error {
checkedCerts := make(map[string]struct{})
for _, chain := range tlsConn.ConnectionState().VerifiedChains {
for _, cert := range chain {
if _, checked := checkedCerts[string(cert.Signature)]; checked {
continue
}
checkedCerts[string(cert.Signature)] = struct{}{}
// Check the expiration.
timeNow := time.Now()
expiresInHours := int64(cert.NotAfter.Sub(timeNow).Hours())
// fmt.Printf("Cert '%s' expires in %d days\n", cert.Subject.CommonName, expiresInHours/24)
if expiresInHours <= 48 {
return fmt.Errorf("certificate '%s' will expire on %s", cert.Subject.CommonName, cert.NotAfter)
}
}
}
return nil
}
func extractParams(addr string) (string, string, error) {
var err error
hostport := strings.Split(addr, ":")
if len(hostport) > 2 {
err = errors.New("too many colons in xmpp server address")
return addr, hostport[0], err
}
// Address is composed of two parts, we are good
if len(hostport) == 2 && hostport[1] != "" {
return addr, hostport[0], err
}
// Port was not passed, we append XMPP default port:
return strings.Join([]string{hostport[0], "5222"}, ":"), hostport[0], err
}
+288
View File
@@ -0,0 +1,288 @@
package xmpp
import (
"encoding/xml"
"errors"
"fmt"
"net"
"time"
"gosrc.io/xmpp/stanza"
)
//=============================================================================
// EventManager
// ConnState represents the current connection state.
type ConnState = uint8
// This is a the list of events happening on the connection that the
// client can be notified about.
const (
StateDisconnected ConnState = iota
StateConnected
StateSessionEstablished
StateStreamError
)
// Event is a structure use to convey event changes related to client state. This
// is for example used to notify the client when the client get disconnected.
type Event struct {
State ConnState
Description string
StreamError string
SMState SMState
}
// SMState holds Stream Management information regarding the session that can be
// used to resume session after disconnect
type SMState struct {
// Stream Management ID
Id string
// Inbound stanza count
Inbound uint
// TODO Store location for IP affinity
// TODO Store max and timestamp, to check if we should retry resumption or not
}
// EventHandler is use to pass events about state of the connection to
// client implementation.
type EventHandler func(Event)
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) disconnected(state SMState) {
em.CurrentState = StateDisconnected
if em.Handler != nil {
em.Handler(Event{State: em.CurrentState, SMState: state})
}
}
func (em EventManager) streamError(error, desc string) {
em.CurrentState = StateStreamError
if em.Handler != nil {
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
}
}
// Client
// ============================================================================
// Client is the main structure used to connect as a client on an XMPP
// server.
type Client struct {
// Store user defined options and states
config Config
// Session gather data that can be accessed by users of this library
Session *Session
// TCP level connection / can be replaced by a TLS session after starttls
conn net.Conn
// Router is used to dispatch packets
router *Router
// Track and broadcast connection state
EventManager
}
/*
Setting up the client / Checking the parameters
*/
// NewClient generates a new XMPP client, based on Config passed as parameters.
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
func NewClient(config Config, r *Router) (c *Client, err error) {
// Parse JID
if config.parsedJid, err = NewJid(config.Jid); err != nil {
err = errors.New("missing jid")
return nil, NewConnError(err, true)
}
if config.Password == "" {
err = errors.New("missing password")
return nil, NewConnError(err, true)
}
// Fallback to jid domain
if config.Address == "" {
config.Address = config.parsedJid.Domain
// Fetch SRV DNS-Entries
_, srvEntries, err := net.LookupSRV("xmpp-client", "tcp", config.parsedJid.Domain)
if err == nil && len(srvEntries) > 0 {
// If we found matching DNS records, use the entry with highest weight
bestSrv := srvEntries[0]
for _, srv := range srvEntries {
if srv.Priority <= bestSrv.Priority && srv.Weight >= bestSrv.Weight {
bestSrv = srv
config.Address = ensurePort(srv.Target, int(srv.Port))
}
}
}
}
config.Address = ensurePort(config.Address, 5222)
c = new(Client)
c.config = config
c.router = r
if c.config.ConnectTimeout == 0 {
c.config.ConnectTimeout = 15 // 15 second as default
}
return
}
// Connect triggers actual TCP connection, based on previously defined parameters.
// Connect simply triggers resumption, with an empty session state.
func (c *Client) Connect() error {
var state SMState
return c.Resume(state)
}
// Resume attempts resuming a Stream Managed session, based on the provided stream management
// state.
func (c *Client) Resume(state SMState) 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, state); err != nil {
return err
}
c.updateState(StateSessionEstablished)
// Start the keepalive go routine
keepaliveQuit := make(chan struct{})
go keepalive(c.conn, keepaliveQuit)
// Start the receiver go routine
state = c.Session.SMState
go c.recv(state, keepaliveQuit)
// We're connected and can now receive and send messages.
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
// TODO: Do we always want to send initial presence automatically ?
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
fmt.Fprintf(c.Session.streamLogger, "<presence/>")
return err
}
func (c *Client) Disconnect() {
_ = c.SendRaw("</stream:stream>")
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
_ = c.conn.Close()
}
func (c *Client) SetHandler(handler EventHandler) {
c.Handler = handler
}
// Send marshals XMPP stanza and sends it to the server.
func (c *Client) Send(packet stanza.Packet) error {
conn := c.conn
if conn == nil {
return errors.New("client is not connected")
}
data, err := xml.Marshal(packet)
if err != nil {
return errors.New("cannot marshal packet " + err.Error())
}
return c.sendWithLogger(string(data))
}
// SendRaw sends an XMPP stanza as a string to the server.
// It can be invalid XML or XMPP content. In that case, the server will
// disconnect the client. It is up to the user of this method to
// carefully craft the XML content to produce valid XMPP.
func (c *Client) SendRaw(packet string) error {
conn := c.conn
if conn == nil {
return errors.New("client is not connected")
}
return c.sendWithLogger(packet)
}
func (c *Client) sendWithLogger(packet string) error {
var err error
_, err = fmt.Fprintf(c.Session.streamLogger, packet)
return err
}
// ============================================================================
// Go routines
// Loop: Receive data from server
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) {
for {
val, err := stanza.NextPacket(c.Session.decoder)
if err != nil {
close(keepaliveQuit)
c.disconnected(state)
return err
}
// Handle stream errors
switch packet := val.(type) {
case stanza.StreamError:
c.router.route(c, val)
close(keepaliveQuit)
c.streamError(packet.Error.Local, packet.Text)
return errors.New("stream error: " + packet.Error.Local)
// Process Stream management nonzas
case stanza.SMRequest:
answer := stanza.SMAnswer{XMLName: xml.Name{
Space: stanza.NSStreamManagement,
Local: "a",
}, H: state.Inbound}
c.Send(answer)
default:
state.Inbound++
}
c.router.route(c, val)
}
}
// Loop: send whitespace keepalive to server
// This is use to keep the connection open, but also to detect connection loss
// and trigger proper client connection shutdown.
func keepalive(conn net.Conn, quit <-chan struct{}) {
// TODO: Make keepalive interval configurable
ticker := time.NewTicker(30 * time.Second)
for {
select {
case <-ticker.C:
if n, err := fmt.Fprintf(conn, "\n"); err != nil || n != 1 {
// When keep alive fails, we force close the connection. In all cases, the recv will also fail.
ticker.Stop()
_ = conn.Close()
return
}
case <-quit:
ticker.Stop()
return
}
}
}
+281
View File
@@ -0,0 +1,281 @@
package xmpp
import (
"encoding/xml"
"errors"
"fmt"
"net"
"testing"
"time"
"gosrc.io/xmpp/stanza"
)
const (
// Default port is not standard XMPP port to avoid interfering
// with local running XMPP server
testXMPPAddress = "localhost:15222"
defaultTimeout = 2 * time.Second
)
func TestClient_Connect(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, handlerConnectSuccess)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(config, router); err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
if err = client.Connect(); err != nil {
t.Errorf("XMPP connection failed: %s", err)
}
mock.Stop()
}
func TestClient_NoInsecure(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, handlerAbortTLS)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(config, router); err != nil {
t.Errorf("cannot create XMPP client: %s", err)
}
if err = client.Connect(); err == nil {
// When insecure is not allowed:
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
}
mock.Stop()
}
// Check that the client is properly tracking features, as session negotiation progresses.
func TestClient_FeaturesTracking(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, handlerAbortTLS)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(config, router); err != nil {
t.Errorf("cannot create XMPP client: %s", err)
}
if err = client.Connect(); err == nil {
// When insecure is not allowed:
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
}
mock.Stop()
}
func TestClient_RFC3921Session(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, handlerConnectWithSession)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(config, router); err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
if err = client.Connect(); err != nil {
t.Errorf("XMPP connection failed: %s", err)
}
mock.Stop()
}
//=============================================================================
// Basic XMPP Server Mock Handlers.
const serverStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' id='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
// Test connection with a basic straightforward workflow
func handlerConnectSuccess(t *testing.T, c net.Conn) {
decoder := xml.NewDecoder(c)
checkOpenStream(t, c, decoder)
sendStreamFeatures(t, c, decoder) // Send initial features
readAuth(t, decoder)
fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
checkOpenStream(t, c, decoder) // Reset stream
sendBindFeature(t, c, decoder) // Send post auth features
bind(t, c, decoder)
}
// We expect client will abort on TLS
func handlerAbortTLS(t *testing.T, c net.Conn) {
decoder := xml.NewDecoder(c)
checkOpenStream(t, c, decoder)
sendStreamFeatures(t, c, decoder) // Send initial features
}
// Test connection with mandatory session (RFC-3921)
func handlerConnectWithSession(t *testing.T, c net.Conn) {
decoder := xml.NewDecoder(c)
checkOpenStream(t, c, decoder)
sendStreamFeatures(t, c, decoder) // Send initial features
readAuth(t, decoder)
fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
checkOpenStream(t, c, decoder) // Reset stream
sendRFC3921Feature(t, c, decoder) // Send post auth features
bind(t, c, decoder)
session(t, c, decoder)
}
func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
c.SetDeadline(time.Now().Add(defaultTimeout))
defer c.SetDeadline(time.Time{})
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
var token xml.Token
token, err := decoder.Token()
if err != nil {
t.Errorf("cannot read next token: %s", err)
}
switch elem := token.(type) {
// Wait for first startElement
case xml.StartElement:
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
return
}
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
t.Errorf("cannot write server stream open: %s", err)
}
return
}
}
}
func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) {
// This is a basic server, supporting only 1 stream feature: SASL Plain Auth
features := `<stream:features>
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>`
if _, err := fmt.Fprintln(c, features); err != nil {
t.Errorf("cannot send stream feature: %s", err)
}
}
// TODO return err in case of error reading the auth params
func readAuth(t *testing.T, decoder *xml.Decoder) string {
se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read auth: %s", err)
return ""
}
var nv interface{}
nv = &stanza.SASLAuth{}
// Decode element into pointer storage
if err = decoder.DecodeElement(nv, &se); err != nil {
t.Errorf("cannot decode auth: %s", err)
return ""
}
switch v := nv.(type) {
case *stanza.SASLAuth:
return v.Value
}
return ""
}
func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
// This is a basic server, supporting only 1 stream feature after auth: resource binding
features := `<stream:features>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
</stream:features>`
if _, err := fmt.Fprintln(c, features); err != nil {
t.Errorf("cannot send stream feature: %s", err)
}
}
func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) {
// This is a basic server, supporting only 2 features after auth: resource & session binding
features := `<stream:features>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
</stream:features>`
if _, err := fmt.Fprintln(c, features); err != nil {
t.Errorf("cannot send stream feature: %s", err)
}
}
func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read bind: %s", err)
return
}
iq := &stanza.IQ{}
// Decode element into pointer storage
if err = decoder.DecodeElement(&iq, &se); err != nil {
t.Errorf("cannot decode bind iq: %s", err)
return
}
// TODO Check all elements
switch iq.Payload.(type) {
case *stanza.Bind:
result := `<iq id='%s' type='result'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<jid>%s</jid>
</bind>
</iq>`
fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID
}
}
func session(t *testing.T, c net.Conn, decoder *xml.Decoder) {
se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read session: %s", err)
return
}
iq := &stanza.IQ{}
// Decode element into pointer storage
if err = decoder.DecodeElement(&iq, &se); err != nil {
t.Errorf("cannot decode session iq: %s", err)
return
}
switch iq.Payload.(type) {
case *stanza.StreamSession:
result := `<iq id='%s' type='result'/>`
fmt.Fprintf(c, result, iq.Id)
}
}
+13
View File
@@ -0,0 +1,13 @@
module gosrc.io/xmpp/cmd
go 1.12
require (
github.com/bdlm/log v0.1.19
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7
github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.0
gosrc.io/xmpp v0.1.1
)
replace gosrc.io/xmpp => ./../
+159
View File
@@ -0,0 +1,159 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/bdlm/log v0.1.19 h1:GqVFZC+khJCEbtTmkaDL/araNDwxTeLBmdMK8pbRoBE=
github.com/bdlm/log v0.1.19/go.mod h1:30V5Zwc5Vt5ePq5rd9KJ6JQ/A5aFUcKzq5fYtO7c9qc=
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 h1:ggZyn+N8eoBh/qLla2kUtqm/ysjnkbzUxTQY+6LMshY=
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7/go.mod h1:E4vIYZDcEPVbE/Dbxc7GpA3YJpXnsF5csRt8LptMGWI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+131
View File
@@ -0,0 +1,131 @@
# sendXMPP
sendxmpp is a tool to send messages from command-line.
## Installation
To install `sendxmpp` in your Go path:
```
$ go get -u gosrc.io/xmpp/cmd/sendxmpp
```
## Usage
```
$ sendxmpp --help
Usage:
sendxmpp <recipient,> [message] [flags]
Examples:
sendxmpp to@chat.sum7.eu "Hello World!"
Flags:
--addr string host[:port]
--config string config file (default is ~/.config/fluxxmpp.yml)
-h, --help help for sendxmpp
--jid string using jid (required)
-m, --muc recipient is a muc (join it before sending messages)
--password string using password for your jid (required)
```
## Examples
Message from arguments:
```bash
$ sendxmpp to@example.org "Hello World!"
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:42:43.310+02:00
info send message
muc=false text="Hello World!" to="to@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:42:43.310+02:00
```
Message from STDIN:
```bash
$ journalctl -f | sendxmpp to@example.org -
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:40:03.177+02:00
info send message
muc=false text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="to@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:40:03.178+02:00
info send message
muc=false text="Jul 17 23:36:46 RECHNERNAME systemd[755]: Started Fetch mails." to="to@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:40:03.178+02:00
^C
```
Multiple recipients:
```bash
$ sendxmpp to1@example.org,to2@example.org "Multiple recipient"
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:47:57.650+02:00
info send message
muc=false text="Multiple recipient" to="to1@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:47:57.651+02:00
info send message
muc=false text="Multiple recipient" to="to2@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:47:57.652+02:00
```
Send to MUC:
```bash
journalctl -f | sendxmpp testit@conference.chat.sum7.eu - --muc
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:52:56.269+02:00
info send message
muc=true text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="testit@conference.chat.sum7.eu"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:52:56.270+02:00
info send message
muc=true text="Jul 17 23:48:58 RECHNERNAME systemd[755]: mail.service: Succeeded." to="testit@conference.chat.sum7.eu"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:52:56.277+02:00
^C
```
### Authentification
#### Configuration file
In `/etc/`, `~/.config` and `.` (here).
You could create the file name `fluxxmpp` with you favorite file extenion (e.g. `toml`, `yml`).
e.g. ~/.config/fluxxmpp.toml
```toml
jid = "bot@example.org"
password = "secret"
addr = "example.com:5222"
```
#### Environment variables
```bash
export FLUXXMPP_JID='bot@example.org';
export FLUXXMPP_PASSWORD='secret';
export FLUXXMPP_ADDR='example.com:5222';
sendxmpp to@example.org "Hello Welt";
```
#### Parameters
Warning: This should not be used for production systems, as all users on the system
can read the running processes, and their parameters (and thus the password).
```bash
sendxmpp to@example.org "Hello World!" --jid bot@example.org --password secret --addr example.com:5222;
```
+14
View File
@@ -0,0 +1,14 @@
# TODO
## Issues
- Remove global variable (like mucToleave)
- Does not report error when trying to connect to a non open port (for example localhost with no server running).
## Features
- configuration
- allow unencrypted
- skip tls verification
- support muc and single user at same time
- send html -> parse console colors to xhtml (is there a easy way or lib for it ?)
+131
View File
@@ -0,0 +1,131 @@
package main
import (
"bufio"
"os"
"strings"
"sync"
"github.com/bdlm/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gosrc.io/xmpp"
)
var configFile = ""
// FIXME: Remove global variables
var isMUCRecipient = false
var cmd = &cobra.Command{
Use: "sendxmpp <recipient,> [message]",
Example: `sendxmpp to@chat.sum7.eu "Hello World!"`,
Args: cobra.ExactArgs(2),
Run: sendxmpp,
}
func sendxmpp(cmd *cobra.Command, args []string) {
receiver := strings.Split(args[0], ",")
msgText := args[1]
var err error
client, err := xmpp.NewClient(xmpp.Config{
Jid: viper.GetString("jid"),
Address: viper.GetString("addr"),
Password: viper.GetString("password"),
}, xmpp.NewRouter())
if err != nil {
log.Errorf("error when starting xmpp client: %s", err)
return
}
wg := sync.WaitGroup{}
wg.Add(1)
// FIXME: Remove global variables
var mucsToLeave []*xmpp.Jid
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
defer wg.Done()
log.Info("client connected")
if isMUCRecipient {
for _, muc := range receiver {
jid, err := xmpp.NewJid(muc)
if err != nil {
log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err)
continue
}
jid.Resource = "sendxmpp"
if err := joinMUC(c, jid); err != nil {
log.WithField("muc", muc).Errorf("error joining muc: %w", err)
continue
}
mucsToLeave = append(mucsToLeave, jid)
}
}
if msgText != "-" {
send(c, receiver, msgText)
return
}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
send(c, receiver, scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Errorf("error on reading stdin: %s", err)
}
})
go func() {
err := cm.Run()
log.Panic("closed connection:", err)
wg.Done()
}()
wg.Wait()
leaveMUCs(client, mucsToLeave)
}
func init() {
cobra.OnInitialize(initConfig)
cmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ~/.config/fluxxmpp.yml)")
cmd.Flags().StringP("jid", "", "", "using jid (required)")
viper.BindPFlag("jid", cmd.Flags().Lookup("jid"))
cmd.Flags().StringP("password", "", "", "using password for your jid (required)")
viper.BindPFlag("password", cmd.Flags().Lookup("password"))
cmd.Flags().StringP("addr", "", "", "host[:port]")
viper.BindPFlag("addr", cmd.Flags().Lookup("addr"))
cmd.Flags().BoolVarP(&isMUCRecipient, "muc", "m", false, "recipient is a muc (join it before sending messages)")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if configFile != "" {
viper.SetConfigFile(configFile)
}
viper.SetConfigName("fluxxmpp")
viper.AddConfigPath("/etc/")
viper.AddConfigPath("$HOME/.config")
viper.AddConfigPath(".")
viper.SetEnvPrefix("FLUXXMPP")
viper.AutomaticEnv()
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err != nil {
log.Warnf("no configuration found (somebody could read your password from process argument list): %s", err)
}
}
+6
View File
@@ -0,0 +1,6 @@
/*
sendxmpp is a command-line tool to send to send XMPP messages to users
*/
package main
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"os"
"github.com/bdlm/log"
stdLogger "github.com/bdlm/std/logger"
)
type hook struct{}
func (h *hook) Fire(entry *log.Entry) error {
switch entry.Level {
case log.PanicLevel:
entry.Logger.Out = os.Stderr
case log.FatalLevel:
entry.Logger.Out = os.Stderr
case log.ErrorLevel:
entry.Logger.Out = os.Stderr
case log.WarnLevel:
entry.Logger.Out = os.Stdout
case log.InfoLevel:
entry.Logger.Out = os.Stdout
case log.DebugLevel:
entry.Logger.Out = os.Stdout
default:
}
return nil
}
func (h *hook) Levels() []stdLogger.Level {
return log.AllLevels
}
+12
View File
@@ -0,0 +1,12 @@
package main
import (
"github.com/bdlm/log"
)
func main() {
log.AddHook(&hook{})
if err := cmd.Execute(); err != nil {
log.Fatal(err)
}
}
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"github.com/bdlm/log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error {
return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()},
Extensions: []stanza.PresExtension{
stanza.MucPresence{
History: stanza.History{MaxStanzas: stanza.NewNullableInt(0)},
}},
})
}
func leaveMUCs(c xmpp.Sender, mucsToLeave []*xmpp.Jid) {
for _, muc := range mucsToLeave {
if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{
To: muc.Full(),
Type: stanza.PresenceTypeUnavailable,
}}); err != nil {
log.WithField("muc", muc).Errorf("error on leaving muc: %s", err)
}
}
}
+36
View File
@@ -0,0 +1,36 @@
package main
import (
"github.com/bdlm/log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func send(c xmpp.Sender, recipient []string, msgText string) {
msg := stanza.Message{
Attrs: stanza.Attrs{Type: stanza.MessageTypeChat},
Body: msgText,
}
if isMUCRecipient {
msg.Type = stanza.MessageTypeGroupchat
}
for _, to := range recipient {
msg.To = to
if err := c.Send(msg); err != nil {
log.WithFields(map[string]interface{}{
"muc": isMUCRecipient,
"to": to,
"text": msgText,
}).Errorf("error on send message: %s", err)
} else {
log.WithFields(map[string]interface{}{
"muc": isMUCRecipient,
"to": to,
"text": msgText,
}).Info("send message")
}
}
}
+49
View File
@@ -0,0 +1,49 @@
# XMPP Check
XMPP check is a tool to check TLS certificate on a remote server.
## Installation
To install `xmpp-check` in your Go path:
```
$ go get -u gosrc.io/xmpp/cmd/xmpp-check
```
## Usage
```
$ xmpp-check --help
Usage:
xmpp-check <host[:port]> [flags]
Examples:
xmpp-check chat.sum7.eu:5222 --domain meckerspace.de
Flags:
-d, --domain string domain if host handle multiple domains
-h, --help help for xmpp-check
```
If you server is on standard port and XMPP domains matches the hostname you can simply use:
```
$ xmpp-check chat.sum7.eu
info All checks passed
⇢ address="chat.sum7.eu" domain=""
⇢ main.go:43 main.runCheck
⇢ 2019-07-16T22:01:39.765+02:00
```
You can also pass the port and the XMPP domain if different from the server hostname:
```
$ xmpp-check chat.sum7.eu:5222 --domain meckerspace.de
info All checks passed
⇢ address="chat.sum7.eu:5222" domain="meckerspace.de"
⇢ main.go:43 main.runCheck
⇢ 2019-07-16T22:01:33.270+02:00
```
Error code will be non-zero in case of error. You can thus use it directly with your usual
monitoring scripts.
+3
View File
@@ -0,0 +1,3 @@
# TODO
- Use a config file to define the checks to perform as client on an XMPP server.
+6
View File
@@ -0,0 +1,6 @@
/*
xmpp-check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires.
*/
package main
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"os"
"github.com/bdlm/log"
stdLogger "github.com/bdlm/std/logger"
)
type hook struct{}
func (h *hook) Fire(entry *log.Entry) error {
switch entry.Level {
case log.PanicLevel:
entry.Logger.Out = os.Stderr
case log.FatalLevel:
entry.Logger.Out = os.Stderr
case log.ErrorLevel:
entry.Logger.Out = os.Stderr
case log.WarnLevel:
entry.Logger.Out = os.Stdout
case log.InfoLevel:
entry.Logger.Out = os.Stdout
case log.DebugLevel:
entry.Logger.Out = os.Stdout
default:
}
return nil
}
func (h *hook) Levels() []stdLogger.Level {
return log.AllLevels
}
+44
View File
@@ -0,0 +1,44 @@
package main
import (
"github.com/bdlm/log"
"github.com/spf13/cobra"
"gosrc.io/xmpp"
)
func main() {
log.AddHook(&hook{})
cmd.Execute()
}
var domain = ""
var cmd = &cobra.Command{
Use: "xmpp-check <host[:port]>",
Example: "xmpp-check chat.sum7.eu:5222 --domain meckerspace.de",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runCheck(args[0], domain)
},
}
func init() {
cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
}
func runCheck(address, domain string) {
logger := log.WithFields(map[string]interface{}{
"address": address,
"domain": domain,
})
client, err := xmpp.NewChecker(address, domain)
if err != nil {
log.Fatal("Error: ", err)
}
if err = client.Check(); err != nil {
logger.Fatal("Failed connection check: ", err)
}
logger.Println("All checks passed")
}
+1
View File
@@ -0,0 +1 @@
comment: off
+5
View File
@@ -0,0 +1,5 @@
build:
build:
image: fluux/build
dockerfile: Dockerfile
encrypted_env_file: codeship.env.encrypted
+5
View File
@@ -0,0 +1,5 @@
- type: serial
steps:
- name: test
service: build
command: ./test.sh
+1
View File
@@ -0,0 +1 @@
yVKgVFeKW6SSnC/KgLYpfYtTcqqTke1gOIW5GUiVvRijnhweOJiYKFPmwPjpt1FVrg4WVELQUNbxn3lmfyHVVF7r
+199
View File
@@ -0,0 +1,199 @@
package xmpp
import (
"crypto/sha1"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"io"
"net"
"time"
"gosrc.io/xmpp/stanza"
)
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
type ComponentOptions struct {
// =================================
// Component Connection Info
// Domain is the XMPP server subdomain that the component will handle
Domain string
// Secret is the "password" used by the XMPP server to secure component access
Secret string
// Address is the XMPP Host and port to connect to. Host is of
// the form 'serverhost:port' i.e "localhost:8888"
Address string
// =================================
// Component discovery
// Component human readable name, that will be shown in XMPP discovery
Name string
// Typical categories and types: https://xmpp.org/registrar/disco-categories.html
Category string
Type string
// =================================
// Communication with developer client / StreamManager
// Track and broadcast connection state
EventManager
}
// Component implements an XMPP extension allowing to extend XMPP server
// using external components. Component specifications are defined
// in XEP-0114, XEP-0355 and XEP-0356.
type Component struct {
ComponentOptions
router *Router
// TCP level connection
conn net.Conn
// read / write
socketProxy io.ReadWriter // TODO
decoder *xml.Decoder
}
func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
c := Component{ComponentOptions: opts, router: r}
return &c, nil
}
// Connect triggers component connection to XMPP server component port.
// TODO: Failed handshake should be a permanent error
func (c *Component) Connect() error {
var conn net.Conn
var err error
if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil {
return err
}
c.conn = conn
// 1. Send stream open tag
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
return errors.New("cannot send stream open " + err.Error())
}
c.decoder = xml.NewDecoder(conn)
// 2. Initialize xml decoder and extract streamID from reply
streamId, err := stanza.InitStream(c.decoder)
if err != nil {
return errors.New("cannot init decoder " + err.Error())
}
// 3. Authentication
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
return errors.New("cannot send handshake " + err.Error())
}
// 4. Check server response for authentication
val, err := stanza.NextPacket(c.decoder)
if err != nil {
return err
}
switch v := val.(type) {
case stanza.StreamError:
return errors.New("handshake failed " + v.Error.Local)
case stanza.Handshake:
// Start the receiver go routine
go c.recv()
return nil
default:
return errors.New("expecting handshake result, got " + v.Name())
}
}
func (c *Component) Resume() error {
return errors.New("components do not support stream management")
}
func (c *Component) Disconnect() {
_ = c.SendRaw("</stream:stream>")
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
_ = c.conn.Close()
}
func (c *Component) SetHandler(handler EventHandler) {
c.Handler = handler
}
// Receiver Go routine receiver
func (c *Component) recv() (err error) {
for {
val, err := stanza.NextPacket(c.decoder)
if err != nil {
c.updateState(StateDisconnected)
return err
}
// Handle stream errors
switch p := val.(type) {
case stanza.StreamError:
c.router.route(c, val)
c.streamError(p.Error.Local, p.Text)
return errors.New("stream error: " + p.Error.Local)
}
c.router.route(c, val)
}
}
// Send marshalls XMPP stanza and sends it to the server.
func (c *Component) Send(packet stanza.Packet) error {
conn := c.conn
if conn == nil {
return errors.New("component is not connected")
}
data, err := xml.Marshal(packet)
if err != nil {
return errors.New("cannot marshal packet " + err.Error())
}
if _, err := fmt.Fprintf(conn, string(data)); err != nil {
return errors.New("cannot send packet " + err.Error())
}
return nil
}
// SendRaw sends an XMPP stanza as a string to the server.
// It can be invalid XML or XMPP content. In that case, the server will
// disconnect the component. It is up to the user of this method to
// carefully craft the XML content to produce valid XMPP.
func (c *Component) SendRaw(packet string) error {
conn := c.conn
if conn == nil {
return errors.New("component is not connected")
}
var err error
_, err = fmt.Fprintf(c.conn, packet)
return err
}
// handshake generates an authentication token based on StreamID and shared secret.
func (c *Component) handshake(streamId string) string {
// 1. Concatenate the Stream ID received from the server with the shared secret.
concatStr := streamId + c.Secret
// 2. Hash the concatenated string according to the SHA1 algorithm, i.e., SHA1( concat (sid, password)).
h := sha1.New()
h.Write([]byte(concatStr))
hash := h.Sum(nil)
// 3. Ensure that the hash output is in hexadecimal format, not binary or base64.
// 4. Convert the hash output to all lowercase characters.
encodedStr := hex.EncodeToString(hash)
return encodedStr
}
/*
TODO: Add support for discovery management directly in component
TODO: Support multiple identities on disco info
TODO: Support returning features on disco info
*/
+25
View File
@@ -0,0 +1,25 @@
package xmpp
import (
"testing"
)
func TestHandshake(t *testing.T) {
opts := ComponentOptions{
Domain: "test.localhost",
Secret: "mypass",
}
c := Component{ComponentOptions: opts}
streamID := "1263952298440005243"
expected := "c77e2ef0109fbbc5161e83b51629cd1353495332"
result := c.handshake(streamID)
if result != expected {
t.Errorf("incorrect handshake calculation '%s' != '%s'", result, expected)
}
}
func TestGenerateHandshake(t *testing.T) {
// TODO
}
+24
View File
@@ -0,0 +1,24 @@
package xmpp
import (
"crypto/tls"
"io"
"os"
)
type Config struct {
Address string
Jid string
parsedJid *Jid // For easier manipulation
Password string
StreamLogger *os.File // Used for debugging
Lang string // TODO: should default to 'en'
ConnectTimeout int // Client timeout in seconds. Default to 15
// tls.Config must not be modified after having been passed to NewClient. The
// Client connect method may override the tls.Config.ServerName if it was not set.
TLSConfig *tls.Config
// Insecure can be set to true to allow to open a session without TLS. If TLS
// is supported on the server, we will still try to use it.
Insecure bool
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
}
+33
View File
@@ -0,0 +1,33 @@
package xmpp
import (
"fmt"
"golang.org/x/xerrors"
)
type ConnError struct {
frame xerrors.Frame
err error
// Permanent will be true if error is not recoverable
Permanent bool
}
func NewConnError(err error, permanent bool) ConnError {
return ConnError{err: err, frame: xerrors.Caller(1), Permanent: permanent}
}
func (e ConnError) Format(s fmt.State, verb rune) {
xerrors.FormatError(e, s, verb)
}
func (e ConnError) FormatError(p xerrors.Printer) error {
e.frame.Format(p)
return e.err
}
func (e ConnError) Error() string {
return fmt.Sprint(e)
}
func (e ConnError) Unwrap() error { return e.err }
+42
View File
@@ -0,0 +1,42 @@
/*
Fluux XMPP is an modern and full-featured XMPP library that can be used to build clients or
server components.
The goal is to make simple to write modern compliant XMPP software:
- For automation (like for example monitoring of an XMPP service),
- For building connected "things" by plugging them on an XMPP server,
- For writing simple chatbots to control a service or a thing.
- For writing XMPP servers components. Fluux XMPP supports:
- XEP-0114: Jabber Component Protocol
- XEP-0355: Namespace Delegation
- XEP-0356: Privileged Entity
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
The library includes a StreamManager that provides features like autoreconnect exponential back-off.
The library is implementing latest versions of the XMPP specifications (RFC 6120 and RFC 6121), and includes
support for many extensions.
Clients
Fluux XMPP can be use to create fully interactive XMPP clients (for
example console-based), but it is more commonly used to build automated
clients (connected devices, automation scripts, chatbots, etc.).
Components
XMPP components can typically be used to extends the features of an XMPP
server, in a portable way, using component protocol over persistent TCP
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
+4 -4
View File
@@ -1,8 +1,8 @@
module github.com/matterbridge/go-xmpp module gosrc.io/xmpp
go 1.21.5 go 1.12
require ( require (
golang.org/x/crypto v0.23.0 github.com/google/go-cmp v0.3.0
golang.org/x/net v0.25.0 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
) )
+4 -4
View File
@@ -1,4 +1,4 @@
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+91
View File
@@ -0,0 +1,91 @@
package xmpp
import (
"fmt"
"strings"
"unicode"
)
type Jid struct {
Node string
Domain string
Resource string
}
func NewJid(sjid string) (*Jid, error) {
jid := new(Jid)
if sjid == "" {
return jid, fmt.Errorf("jid cannot be empty")
}
s1 := strings.SplitN(sjid, "@", 2)
if len(s1) == 1 { // This is a server or component JID
jid.Domain = s1[0]
} else { // JID has a local username part
if s1[0] == "" {
return jid, fmt.Errorf("invalid jid '%s", sjid)
}
jid.Node = s1[0]
if s1[1] == "" {
return jid, fmt.Errorf("domain cannot be empty")
}
jid.Domain = s1[1]
}
// Extract resource from domain field
s2 := strings.SplitN(jid.Domain, "/", 2)
if len(s2) == 2 { // If len = 1, domain is already correct, and resource is already empty
jid.Domain = s2[0]
jid.Resource = s2[1]
}
if !isUsernameValid(jid.Node) {
return jid, fmt.Errorf("invalid Node in JID '%s'", sjid)
}
if !isDomainValid(jid.Domain) {
return jid, fmt.Errorf("invalid domain in JID '%s'", sjid)
}
return jid, nil
}
func (j *Jid) Full() string {
return j.Node + "@" + j.Domain + "/" + j.Resource
}
func (j *Jid) Bare() string {
return j.Node + "@" + j.Domain
}
// ============================================================================
// Helpers, for parsing / validation
func isUsernameValid(username string) bool {
invalidRunes := []rune{'@', '/', '\'', '"', ':', '<', '>'}
return strings.IndexFunc(username, isInvalid(invalidRunes)) < 0
}
func isDomainValid(domain string) bool {
if len(domain) == 0 {
return false
}
invalidRunes := []rune{'@', '/'}
return strings.IndexFunc(domain, isInvalid(invalidRunes)) < 0
}
func isInvalid(invalidRunes []rune) func(c rune) bool {
isInvalid := func(c rune) bool {
if unicode.IsSpace(c) {
return true
}
for _, r := range invalidRunes {
if c == r {
return true
}
}
return false
}
return isInvalid
}
+86
View File
@@ -0,0 +1,86 @@
package xmpp
import (
"testing"
)
func TestValidJids(t *testing.T) {
tests := []struct {
jidstr string
expected Jid
}{
{jidstr: "test@domain.com", expected: Jid{"test", "domain.com", ""}},
{jidstr: "test@domain.com/resource", expected: Jid{"test", "domain.com", "resource"}},
// resource can contain '/' or '@'
{jidstr: "test@domain.com/a/b", expected: Jid{"test", "domain.com", "a/b"}},
{jidstr: "test@domain.com/a@b", expected: Jid{"test", "domain.com", "a@b"}},
{jidstr: "domain.com", expected: Jid{"", "domain.com", ""}},
}
for _, tt := range tests {
jid, err := NewJid(tt.jidstr)
if err != nil {
t.Errorf("could not parse correct jid: %s", tt.jidstr)
continue
}
if jid == nil {
t.Error("jid should not be nil")
}
if jid.Node != tt.expected.Node {
t.Errorf("incorrect jid Node (%s): %s", tt.expected.Node, jid.Node)
}
if jid.Node != tt.expected.Node {
t.Errorf("incorrect jid domain (%s): %s", tt.expected.Domain, jid.Domain)
}
if jid.Resource != tt.expected.Resource {
t.Errorf("incorrect jid resource (%s): %s", tt.expected.Resource, jid.Resource)
}
}
}
func TestIncorrectJids(t *testing.T) {
badJids := []string{
"",
"user@",
"@domain.com",
"user:name@domain.com",
"user<name@domain.com",
"test@domain.com@otherdomain.com",
"test@domain com/resource",
}
for _, sjid := range badJids {
if _, err := NewJid(sjid); err == nil {
t.Error("parsing incorrect jid should return error: " + sjid)
}
}
}
func TestFull(t *testing.T) {
jid := "test@domain.com/my resource"
parsedJid, err := NewJid(jid)
if err != nil {
t.Errorf("could not parse jid: %v", err)
}
fullJid := parsedJid.Full()
if fullJid != jid {
t.Errorf("incorrect full jid: %s", fullJid)
}
}
func TestBare(t *testing.T) {
jid := "test@domain.com"
fullJid := jid + "/my resource"
parsedJid, err := NewJid(fullJid)
if err != nil {
t.Errorf("could not parse jid: %v", err)
}
bareJid := parsedJid.Bare()
if bareJid != jid {
t.Errorf("incorrect bare jid: %s", bareJid)
}
}
+32
View File
@@ -0,0 +1,32 @@
package xmpp
import (
"strconv"
"strings"
)
// ensurePort adds a port to an address if none are provided.
// It handles both IPV4 and IPV6 addresses.
func ensurePort(addr string, port int) string {
// This is an IPV6 address literal
if strings.HasPrefix(addr, "[") {
// if address has no port (behind his ipv6 address) - add default port
if strings.LastIndex(addr, ":") <= strings.LastIndex(addr, "]") {
return addr + ":" + strconv.Itoa(port)
}
return addr
}
// This is either an IPV6 address without bracket or an IPV4 address
switch strings.Count(addr, ":") {
case 0:
// This is IPV4 without port
return addr + ":" + strconv.Itoa(port)
case 1:
// This is IPV$ with port
return addr
default:
// This is IPV6 without port, as you need to use bracket with port in IPV6
return "[" + addr + "]:" + strconv.Itoa(port)
}
}
+35
View File
@@ -0,0 +1,35 @@
package xmpp
import (
"testing"
)
type params struct {
}
func TestParseAddr(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{name: "ipv4-no-port-1", input: "localhost", want: "localhost:5222"},
{name: "ipv4-with-port-1", input: "localhost:5555", want: "localhost:5555"},
{name: "ipv4-no-port-2", input: "127.0.0.1", want: "127.0.0.1:5222"},
{name: "ipv4-with-port-2", input: "127.0.0.1:5555", want: "127.0.0.1:5555"},
{name: "ipv6-no-port-1", input: "::1", want: "[::1]:5222"},
{name: "ipv6-no-port-2", input: "[::1]", want: "[::1]:5222"},
{name: "ipv6-no-port-3", input: "2001::7334", want: "[2001::7334]:5222"},
{name: "ipv6-no-port-4", input: "2001:db8:85a3:0:0:8a2e:370:7334", want: "[2001:db8:85a3:0:0:8a2e:370:7334]:5222"},
{name: "ipv6-with-port-1", input: "[::1]:5555", want: "[::1]:5555"},
}
for _, tc := range tests {
t.Run(tc.name, func(st *testing.T) {
addr := ensurePort(tc.input, 5222)
if addr != tc.want {
st.Errorf("incorrect Result: %v (!= %v)", addr, tc.want)
}
})
}
}
+258
View File
@@ -0,0 +1,258 @@
package xmpp
import (
"encoding/xml"
"strings"
"gosrc.io/xmpp/stanza"
)
/*
The XMPP router helps client and component developers select which XMPP they would like to process,
and associate processing code depending on the router configuration.
Here are important rules to keep in mind while setting your routes and matchers:
- Routes are evaluated in the order they are set.
- When a route matches, it is executed and all others routes are ignored. For each packet, only a single
route is executed.
- An empty route will match everything. Adding an empty route as the last route in your router will
allow you to get all stanzas that did not match any previous route. You can for example use this to
log all unexpected stanza received by your client or component.
TODO: Automatically reply to IQ that do not match any route, to comply to XMPP standard.
*/
type Router struct {
// Routes to be matched, in order.
routes []*Route
}
// NewRouter returns a new router instance.
func NewRouter() *Router {
return &Router{}
}
// route is called by the XMPP client to dispatch stanza received using the set up routes.
// It is also used by test, but is not supposed to be used directly by users of the library.
func (r *Router) route(s Sender, p stanza.Packet) {
var match RouteMatch
if r.Match(p, &match) {
// If we match, route the packet
match.Handler.HandlePacket(s, p)
return
}
// If there is no match and we receive an iq set or get, we need to send a reply
if iq, ok := p.(stanza.IQ); ok {
if iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet {
iqNotImplemented(s, iq)
}
}
}
func iqNotImplemented(s Sender, iq stanza.IQ) {
err := stanza.Err{
XMLName: xml.Name{Local: "error"},
Code: 501,
Type: "cancel",
Reason: "feature-not-implemented",
}
reply := iq.MakeError(err)
_ = s.Send(reply)
}
// NewRoute registers an empty routes
func (r *Router) NewRoute() *Route {
route := &Route{}
r.routes = append(r.routes, route)
return route
}
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
for _, route := range r.routes {
if route.Match(p, match) {
return true
}
}
return false
}
// Handle registers a new route with a matcher for a given packet name (iq, message, presence)
// See Route.Packet() and Route.Handler().
func (r *Router) Handle(name string, handler Handler) *Route {
return r.NewRoute().Packet(name).Handler(handler)
}
// HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence)
// See Route.Path() and Route.HandlerFunc().
func (r *Router) HandleFunc(name string, f func(s Sender, p stanza.Packet)) *Route {
return r.NewRoute().Packet(name).HandlerFunc(f)
}
// ============================================================================
// Route
type Handler interface {
HandlePacket(s Sender, p stanza.Packet)
}
type Route struct {
handler Handler
// Matchers are used to "specialize" routes and focus on specific packet features
matchers []matcher
}
func (r *Route) Handler(handler Handler) *Route {
r.handler = handler
return r
}
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as XMPP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(s Sender, p stanza.Packet)
// HandlePacket calls f(s, p)
func (f HandlerFunc) HandlePacket(s Sender, p stanza.Packet) {
f(s, p)
}
// HandlerFunc sets a handler function for the route
func (r *Route) HandlerFunc(f HandlerFunc) *Route {
return r.Handler(f)
}
// addMatcher adds a matcher to the route
func (r *Route) addMatcher(m matcher) *Route {
r.matchers = append(r.matchers, m)
return r
}
func (r *Route) Match(p stanza.Packet, match *RouteMatch) bool {
for _, m := range r.matchers {
if matched := m.Match(p, match); !matched {
return false
}
}
// We have a match, let's pass info route match info
match.Route = r
match.Handler = r.handler
return true
}
// --------------------
// Match on packet name
type nameMatcher string
func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
var name string
// TODO: To avoid type switch everywhere in matching, I think we will need to have
// to move to a concrete type for packets, to make matching and comparison more natural.
// Current code structure is probably too rigid.
// Maybe packet types should even be from an enum.
switch p.(type) {
case stanza.Message:
name = "message"
case stanza.IQ:
name = "iq"
case stanza.Presence:
name = "presence"
}
if name == string(n) {
return true
}
return false
}
// Packet matches on a packet name (iq, message, presence, ...)
// It matches on the Local part of the xml.Name
func (r *Route) Packet(name string) *Route {
name = strings.ToLower(name)
return r.addMatcher(nameMatcher(name))
}
// -------------------------
// Match on stanza type
// nsTypeMather matches on a list of IQ payload namespaces
type nsTypeMatcher []string
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
var stanzaType stanza.StanzaType
switch packet := p.(type) {
case stanza.IQ:
stanzaType = packet.Type
case stanza.Presence:
stanzaType = packet.Type
case stanza.Message:
if packet.Type == "" {
// optional on message, normal is the default type
stanzaType = "normal"
} else {
stanzaType = packet.Type
}
default:
return false
}
return matchInArray(m, string(stanzaType))
}
// IQNamespaces adds an IQ matcher, expecting both an IQ and a
func (r *Route) StanzaType(types ...string) *Route {
for k, v := range types {
types[k] = strings.ToLower(v)
}
return r.addMatcher(nsTypeMatcher(types))
}
// -------------------------
// Match on IQ and namespace
// nsIqMather matches on a list of IQ payload namespaces
type nsIQMatcher []string
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
iq, ok := p.(stanza.IQ)
if !ok {
return false
}
if iq.Payload == nil {
return false
}
return matchInArray(m, iq.Payload.Namespace())
}
// IQNamespaces adds an IQ matcher, expecting both an IQ and a
func (r *Route) IQNamespaces(namespaces ...string) *Route {
for k, v := range namespaces {
namespaces[k] = strings.ToLower(v)
}
return r.addMatcher(nsIQMatcher(namespaces))
}
// ============================================================================
// Matchers
// Matchers are used to "specialize" routes and focus on specific packet features
type matcher interface {
Match(stanza.Packet, *RouteMatch) bool
}
// RouteMatch extracts and gather match information
type RouteMatch struct {
Route *Route
Handler Handler
}
// matchInArray is a generic matching function to check if a string is a list
// of specific function
func matchInArray(arr []string, value string) bool {
for _, str := range arr {
if str == value {
return true
}
}
return false
}
+252
View File
@@ -0,0 +1,252 @@
package xmpp
import (
"bytes"
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// ============================================================================
// Test route & matchers
func TestNameMatcher(t *testing.T) {
router := NewRouter()
router.HandleFunc("message", func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that a message packet is properly matched
conn := NewSenderMock()
msg := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, To: "test@localhost", Id: "1"})
msg.Body = "Hello"
router.route(conn, msg)
if conn.String() != successFlag {
t.Error("Message was not matched and routed properly")
}
// Check that an IQ packet is not matched
conn = NewSenderMock()
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
iq.Payload = &stanza.DiscoInfo{}
router.route(conn, iq)
if conn.String() == successFlag {
t.Error("IQ should not have been matched and routed")
}
}
func TestIQNSMatcher(t *testing.T) {
router := NewRouter()
router.NewRoute().
IQNamespaces(stanza.NSDiscoInfo, stanza.NSDiscoItems).
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that an IQ with proper namespace does match
conn := NewSenderMock()
iqDisco := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
// TODO: Add a function to generate payload with proper namespace initialisation
iqDisco.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
}}
router.route(conn, iqDisco)
if conn.String() != successFlag {
t.Errorf("IQ should have been matched and routed: %v", iqDisco)
}
// Check that another namespace is not matched
conn = NewSenderMock()
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
// TODO: Add a function to generate payload with proper namespace initialisation
iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
}}
router.route(conn, iqVersion)
if conn.String() == successFlag {
t.Errorf("IQ should not have been matched and routed: %v", iqVersion)
}
}
func TestTypeMatcher(t *testing.T) {
router := NewRouter()
router.NewRoute().
StanzaType("normal").
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that a packet with the proper type matches
conn := NewSenderMock()
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
message.Body = "hello"
router.route(conn, message)
if conn.String() != successFlag {
t.Errorf("'normal' message should have been matched and routed: %v", message)
}
// We should match on default type 'normal' for message without a type
conn = NewSenderMock()
message = stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
message.Body = "hello"
router.route(conn, message)
if conn.String() != successFlag {
t.Errorf("message should have been matched and routed: %v", message)
}
// We do not match on other types
conn = NewSenderMock()
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
}}
router.route(conn, iqVersion)
if conn.String() == successFlag {
t.Errorf("iq get should not have been matched and routed: %v", iqVersion)
}
}
func TestCompositeMatcher(t *testing.T) {
router := NewRouter()
router.NewRoute().
IQNamespaces("jabber:iq:version").
StanzaType("get").
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Data set
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
getVersionIq.Payload = &stanza.Version{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
}}
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
setVersionIq.Payload = &stanza.Version{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
}}
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
GetDiscoIq.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "http://jabber.org/protocol/disco#info",
Local: "query",
}}
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
message.Body = "hello"
tests := []struct {
name string
input stanza.Packet
want bool
}{
{name: "match get version iq", input: getVersionIq, want: true},
{name: "ignore set version iq", input: setVersionIq, want: false},
{name: "ignore get discoinfo iq", input: GetDiscoIq, want: false},
{name: "ignore message", input: message, want: false},
}
//
for _, tc := range tests {
t.Run(tc.name, func(st *testing.T) {
conn := NewSenderMock()
router.route(conn, tc.input)
res := conn.String() == successFlag
if tc.want != res {
st.Errorf("incorrect result for %#v\nMatch = %#v, expecting %#v", tc.input, res, tc.want)
}
})
}
}
// A blank route with empty matcher will always match
// It can be use to receive all packets that do not match any of the previous route.
func TestCatchallMatcher(t *testing.T) {
router := NewRouter()
router.NewRoute().
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that we match on several packets
conn := NewSenderMock()
message := stanza.NewMessage(stanza.Attrs{Type: "chat", To: "test@localhost", Id: "1"})
message.Body = "hello"
router.route(conn, message)
if conn.String() != successFlag {
t.Errorf("chat message should have been matched and routed: %v", message)
}
conn = NewSenderMock()
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
}}
router.route(conn, iqVersion)
if conn.String() != successFlag {
t.Errorf("iq get should have been matched and routed: %v", iqVersion)
}
}
// ============================================================================
// SenderMock
var successFlag = "matched"
type SenderMock struct {
buffer *bytes.Buffer
}
func NewSenderMock() SenderMock {
return SenderMock{buffer: new(bytes.Buffer)}
}
func (s SenderMock) Send(packet stanza.Packet) error {
out, err := xml.Marshal(packet)
if err != nil {
return err
}
s.buffer.Write(out)
return nil
}
func (s SenderMock) SendRaw(str string) error {
s.buffer.WriteString(str)
return nil
}
func (s SenderMock) String() string {
return s.buffer.String()
}
func TestSenderMock(t *testing.T) {
conn := NewSenderMock()
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
msg.Body = "Hello"
if err := conn.Send(msg); err != nil {
t.Error("Could not send message")
}
if conn.String() != "<message id=\"1\" to=\"test@localhost\"><body>Hello</body></message>" {
t.Errorf("Incorrect packet sent: %s", conn.String())
}
}
+279
View File
@@ -0,0 +1,279 @@
package xmpp
import (
"crypto/tls"
"encoding/xml"
"errors"
"fmt"
"io"
"net"
"gosrc.io/xmpp/stanza"
)
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
type Session struct {
// Session info
BindJid string // Jabber ID as provided by XMPP server
StreamId string
SMState SMState
Features stanza.StreamFeatures
TlsEnabled bool
lastPacketId int
// read / write
streamLogger io.ReadWriter
decoder *xml.Decoder
// error management
err error
}
func NewSession(conn net.Conn, o Config, state SMState) (net.Conn, *Session, error) {
s := new(Session)
s.SMState = state
s.init(conn, o)
// starttls
var tlsConn net.Conn
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o)
if s.err != nil {
return nil, nil, NewConnError(s.err, true)
}
if !s.TlsEnabled && !o.Insecure {
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
return nil, nil, NewConnError(err, true)
}
if s.TlsEnabled {
s.reset(conn, tlsConn, o)
}
// auth
s.auth(o)
s.reset(tlsConn, tlsConn, o)
// attempt resumption
if s.resume(o) {
return tlsConn, s, s.err
}
// otherwise, bind resource and 'start' XMPP session
s.bind(o)
s.rfc3921Session(o)
// Enable stream management if supported
s.EnableStreamManagement(o)
return tlsConn, s, s.err
}
func (s *Session) PacketId() string {
s.lastPacketId++
return fmt.Sprintf("%x", s.lastPacketId)
}
func (s *Session) init(conn net.Conn, o Config) {
s.setStreamLogger(nil, conn, o)
s.Features = s.open(o.parsedJid.Domain)
}
func (s *Session) reset(conn net.Conn, newConn net.Conn, o Config) {
if s.err != nil {
return
}
s.setStreamLogger(conn, newConn, o)
s.Features = s.open(o.parsedJid.Domain)
}
func (s *Session) setStreamLogger(conn net.Conn, newConn net.Conn, o Config) {
if newConn != conn {
s.streamLogger = newStreamLogger(newConn, o.StreamLogger)
}
s.decoder = xml.NewDecoder(s.streamLogger)
s.decoder.CharsetReader = o.CharsetReader
}
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
// Send stream open tag
if _, s.err = fmt.Fprintf(s.streamLogger, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil {
return
}
// Set xml decoder and extract streamID from reply
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
if s.err != nil {
return
}
// extract stream features
if s.err = s.decoder.Decode(&f); s.err != nil {
s.err = errors.New("stream open decode features: " + s.err.Error())
}
return
}
func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) net.Conn {
if s.err != nil {
return conn
}
if _, ok := s.Features.DoesStartTLS(); ok {
fmt.Fprintf(s.streamLogger, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
var k stanza.TLSProceed
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
return conn
}
if o.TLSConfig == nil {
o.TLSConfig = &tls.Config{}
}
if o.TLSConfig.ServerName == "" {
o.TLSConfig.ServerName = domain
}
tlsConn := tls.Client(conn, o.TLSConfig)
// We convert existing connection to TLS
if s.err = tlsConn.Handshake(); s.err != nil {
return tlsConn
}
if !o.TLSConfig.InsecureSkipVerify {
s.err = tlsConn.VerifyHostname(domain)
}
if s.err == nil {
s.TlsEnabled = true
}
return tlsConn
}
// If we do not allow cleartext connections, make it explicit that server do not support starttls
if !o.Insecure {
s.err = errors.New("XMPP server does not advertise support for starttls")
}
// starttls is not supported => we do not upgrade the connection:
return conn
}
func (s *Session) auth(o Config) {
if s.err != nil {
return
}
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
}
// Attempt to resume session using stream management
func (s *Session) resume(o Config) bool {
if !s.Features.DoesStreamManagement() {
return false
}
if s.SMState.Id == "" {
return false
}
fmt.Fprintf(s.streamLogger, "<resume xmlns='%s' h='%d' previd='%s'/>",
stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id)
var packet stanza.Packet
packet, s.err = stanza.NextPacket(s.decoder)
if s.err == nil {
switch p := packet.(type) {
case stanza.SMResumed:
if p.PrevId != s.SMState.Id {
s.err = errors.New("session resumption: mismatched id")
s.SMState = SMState{}
return false
}
return true
case stanza.SMFailed:
default:
s.err = errors.New("unexpected reply to SM resume")
}
}
s.SMState = SMState{}
return false
}
func (s *Session) bind(o Config) {
if s.err != nil {
return
}
// Send IQ message asking to bind to the local user name.
var resource = o.parsedJid.Resource
if resource != "" {
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
s.PacketId(), stanza.NSBind, resource)
} else {
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
}
var iq stanza.IQ
if s.err = s.decoder.Decode(&iq); s.err != nil {
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
return
}
// TODO Check all elements
switch payload := iq.Payload.(type) {
case *stanza.Bind:
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
default:
s.err = errors.New("iq bind result missing")
}
return
}
// After the bind, if the session is not optional (as per old RFC 3921), we send the session open iq.
func (s *Session) rfc3921Session(o Config) {
if s.err != nil {
return
}
var iq stanza.IQ
// We only negotiate session binding if it is mandatory, we skip it when optional.
if !s.Features.Session.IsOptional() {
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
if s.err = s.decoder.Decode(&iq); s.err != nil {
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
return
}
}
}
// Enable stream management, with session resumption, if supported.
func (s *Session) EnableStreamManagement(o Config) {
if s.err != nil {
return
}
if !s.Features.DoesStreamManagement() {
return
}
fmt.Fprintf(s.streamLogger, "<enable xmlns='%s' resume='true'/>", stanza.NSStreamManagement)
var packet stanza.Packet
packet, s.err = stanza.NextPacket(s.decoder)
if s.err == nil {
switch p := packet.(type) {
case stanza.SMEnabled:
s.SMState = SMState{Id: p.Id}
case stanza.SMFailed:
// TODO: Store error in SMState, for later inspection
default:
s.err = errors.New("unexpected reply to SM enable")
}
}
return
}
+142
View File
@@ -0,0 +1,142 @@
# XMPP Stanza
XMPP `stanza` package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
## Stanza creation
When creating stanzas, you can use two approaches:
1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the
stanza.
2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the
final stanza.
The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of
struct that you can build by hand.
### Composing stanzas manually with structs
Here is for example how you would generate an IQ discovery result:
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
identity := stanza.Identity{
Name: opts.Name,
Category: opts.Category,
Type: opts.Type,
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
},
}
iqResp.Payload = &payload
### Using helpers
Here is for example how you would generate an IQ discovery result using Builder:
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
disco := iq.DiscoInfo()
disco.AddIdentity("Test Component", "gateway", "service")
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
## Payload and extensions
### Message
Here is the list of implemented message extensions:
- `Delegation`
- `Markable`
- `MarkAcknowledged`
- `MarkDisplayed`
- `MarkReceived`
- `StateActive`
- `StateComposing`
- `StateGone`
- `StateInactive`
- `StatePaused`
- `HTML`
- `OOB`
- `ReceiptReceived`
- `ReceiptRequest`
- `Mood`
### Presence
Here is the list of implemented presence extensions:
- `MucPresence`
### IQ
IQ (Information Queries) contain a payload associated with the request and possibly an error. The main difference with
Message and Presence extension is that you can only have one payload per IQ. The XMPP specification does not support
having multiple payloads.
Here is the list of structs implementing IQPayloads:
- `ControlSet`
- `ControlSetResponse`
- `Delegation`
- `DiscoInfo`
- `DiscoItems`
- `Pubsub`
- `Version`
- `Node`
Finally, when the payload of the parsed stanza is unknown, the parser will provide the unknown payload as a generic
`Node` element. You can also use the Node struct to add custom information on stanza generation. However, in both cases,
you may also consider [adding your own custom extensions on stanzas]().
## Adding your own custom extensions on stanzas
Extensions are registered on launch using the `Registry`. It can be used to register you own custom payload. You may
want to do so to support extensions we did not yet implement, or to add your own custom extensions to your XMPP stanzas.
To create an extension you need:
1. to create a struct for that extension. It need to have XMLName for consistency and to tagged at the struct level with
`xml` info.
2. It need to implement one or several extensions interface: stanza.IQPayload, stanza.MsgExtension and / or
stanza.PresExtension
3. Add that custom extension to the stanza.TypeRegistry during the file init.
Here an example code showing how to create a custom IQPayload.
```go
package myclient
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
type CustomPayload struct {
XMLName xml.Name `xml:"my:custom:payload query"`
Node string `xml:"node,attr,omitempty"`
}
func (c CustomPayload) Namespace() string {
return c.XMLName.Space
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
}
```
+91
View File
@@ -0,0 +1,91 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Handshake Stanza
// Handshake is a stanza used by XMPP components to authenticate on XMPP
// component port.
type Handshake struct {
XMLName xml.Name `xml:"jabber:component:accept handshake"`
// TODO Add handshake value with test for proper serialization
// Value string `xml:",innerxml"`
}
func (Handshake) Name() string {
return "component:handshake"
}
// Handshake decoding wrapper
type handshakeDecoder struct{}
var handshake handshakeDecoder
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
var packet Handshake
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// Component delegation
// XEP-0355
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
// depending on the context.
type Delegation struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
Delegated *Delegated // This is used in a message to confirm delegated namespace
}
func (d *Delegation) Namespace() string {
return d.XMLName.Space
}
// Forwarded is used to wrapped forwarded stanzas.
// TODO: Move it in another file, as it is not limited to components.
type Forwarded struct {
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
Stanza Packet
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if packet, err := decodeClient(d, tt); err == nil {
f.Stanza = packet
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
type Delegated struct {
XMLName xml.Name `xml:"delegated"`
Namespace string `xml:"namespace,attr,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
}
+79
View File
@@ -0,0 +1,79 @@
package stanza
import (
"encoding/xml"
"testing"
)
// We should be able to properly parse delegation confirmation messages
func TestParsingDelegationMessage(t *testing.T) {
packetStr := `<message to='service.localhost' from='localhost'>
<delegation xmlns='urn:xmpp:delegation:1'>
<delegated namespace='http://jabber.org/protocol/pubsub'/>
</delegation>
</message>`
var msg Message
data := []byte(packetStr)
if err := xml.Unmarshal(data, &msg); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as MsgExtension
var nsDelegated string
for _, ext := range msg.Extensions {
if delegation, ok := ext.(*Delegation); ok {
nsDelegated = delegation.Delegated.Namespace
}
}
if nsDelegated != "http://jabber.org/protocol/pubsub" {
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
}
}
// Check that we can parse a delegation IQ.
// The most important thing is to be able to
func TestParsingDelegationIQ(t *testing.T) {
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
<delegation xmlns='urn:xmpp:delegation:1'>
<forwarded xmlns='urn:xmpp:forward:0'>
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='http://jabber.org/protocol/mood'>
<item id='current'>
<mood xmlns='http://jabber.org/protocol/mood'>
<excited/>
</mood>
</item>
</publish>
</pubsub>
</iq>
</forwarded>
</delegation>
</iq>`
var iq IQ
data := []byte(packetStr)
if err := xml.Unmarshal(data, &iq); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as IQPayload
var node string
if iq.Payload != nil {
if delegation, ok := iq.Payload.(*Delegation); ok {
packet := delegation.Forwarded.Stanza
forwardedIQ, ok := packet.(IQ)
if !ok {
t.Errorf("Could not extract packet IQ")
return
}
if forwardedIQ.Payload != nil {
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
node = pubsub.Publish.Node
}
}
}
}
if node != "http://jabber.org/protocol/mood" {
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
}
}
+4
View File
@@ -0,0 +1,4 @@
/*
XMPP stanza package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
*/
package stanza
+110
View File
@@ -0,0 +1,110 @@
package stanza
import (
"encoding/xml"
"strconv"
)
// ============================================================================
// XMPP Errors
// Err is an XMPP stanza payload that is used to report error on message,
// presence or iq stanza.
// It is intended to be added in the payload of the erroneous stanza.
type Err struct {
XMLName xml.Name `xml:"error"`
Code int `xml:"code,attr,omitempty"`
Type ErrorType `xml:"type,attr"` // required
Reason string
Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
}
// UnmarshalXML implements custom parsing for XMPP errors
func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
x.XMLName = start.Name
// Extract attributes
for _, attr := range start.Attr {
if attr.Name.Local == "type" {
x.Type = ErrorType(attr.Value)
}
if attr.Name.Local == "code" {
if code, err := strconv.Atoi(attr.Value); err == nil {
x.Code = code
}
}
}
// Check subelements to extract error text and reason (from local namespace).
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
elt := new(Node)
err = d.DecodeElement(elt, &tt)
if err != nil {
return err
}
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
if elt.XMLName == textName {
x.Text = string(elt.Content)
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
x.Reason = elt.XMLName.Local
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
if x.Code == 0 {
return nil
}
// Encode start element and attributes
start.Name = xml.Name{Local: "error"}
code := xml.Attr{
Name: xml.Name{Local: "code"},
Value: strconv.Itoa(x.Code),
}
start.Attr = append(start.Attr, code)
if len(x.Type) > 0 {
typ := xml.Attr{
Name: xml.Name{Local: "type"},
Value: string(x.Type),
}
start.Attr = append(start.Attr, typ)
}
err = e.EncodeToken(start)
// SubTags
// Reason
if x.Reason != "" {
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
e.EncodeToken(xml.StartElement{Name: reason})
e.EncodeToken(xml.EndElement{Name: reason})
}
// Text
if x.Text != "" {
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
e.EncodeToken(xml.StartElement{Name: text})
e.EncodeToken(xml.CharData(x.Text))
e.EncodeToken(xml.EndElement{Name: text})
}
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
+13
View File
@@ -0,0 +1,13 @@
package stanza
// ErrorType is a Enum of error attribute type
type ErrorType string
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
const (
ErrorTypeAuth ErrorType = "auth"
ErrorTypeCancel ErrorType = "cancel"
ErrorTypeContinue ErrorType = "continue"
ErrorTypeModify ErrorType = "modify"
ErrorTypeWait ErrorType = "wait"
)
+39
View File
@@ -0,0 +1,39 @@
package stanza
import (
"encoding/xml"
)
type ControlSet struct {
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
Fields []ControlField `xml:",any"`
}
func (c *ControlSet) Namespace() string {
return c.XMLName.Space
}
type ControlGetForm struct {
XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
}
type ControlField struct {
XMLName xml.Name
Name string `xml:"name,attr,omitempty"`
Value string `xml:"value,attr,omitempty"`
}
type ControlSetResponse struct {
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
}
func (c *ControlSetResponse) Namespace() string {
return c.XMLName.Space
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
}
+26
View File
@@ -0,0 +1,26 @@
package stanza
import (
"encoding/xml"
"testing"
)
func TestControlSet(t *testing.T) {
packet := `
<iq to='test@localhost/jukebox' from='admin@localhost/mbp' type='set' id='2'>
<set xmlns='urn:xmpp:iot:control' xml:lang='en'>
<string name='action' value='play'/>
<string name='url' value='https://soundcloud.com/radiohead/spectre'/>
</set>
</iq>`
parsedIQ := IQ{}
data := []byte(packet)
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
t.Errorf("Paylod is not an iot control set: %v", cs)
}
}
+128
View File
@@ -0,0 +1,128 @@
package stanza
import (
"encoding/xml"
)
/*
TODO support ability to put Raw payload inside IQ
*/
// ============================================================================
// IQ Packet
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
type IQ struct { // Info/Query
XMLName xml.Name `xml:"iq"`
// MUST have a ID
Attrs
// We can only have one payload on IQ:
// "An IQ stanza of type "get" or "set" MUST contain exactly one
// child element, which specifies the semantics of the particular
// request."
Payload IQPayload `xml:",omitempty"`
Error Err `xml:"error,omitempty"`
// Any is used to decode unknown payload as a generique structure
Any *Node `xml:",any"`
}
type IQPayload interface {
Namespace() string
}
func NewIQ(a Attrs) IQ {
// TODO generate IQ ID if not set
// TODO ensure that type is set, as it is required
return IQ{
XMLName: xml.Name{Local: "iq"},
Attrs: a,
}
}
func (iq IQ) MakeError(xerror Err) IQ {
from := iq.From
to := iq.To
iq.Type = "error"
iq.From = to
iq.To = from
iq.Error = xerror
return iq
}
func (IQ) Name() string {
return "iq"
}
type iqDecoder struct{}
var iq iqDecoder
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
var packet IQ
err := p.DecodeElement(&packet, &se)
return packet, err
}
// UnmarshalXML implements custom parsing for IQs
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.XMLName = start.Name
// Extract IQ attributes
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
iq.Id = attr.Value
}
if attr.Name.Local == "type" {
iq.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
iq.To = attr.Value
}
if attr.Name.Local == "from" {
iq.From = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if tt.Name.Local == "error" {
var xmppError Err
err = d.DecodeElement(&xmppError, &tt)
if err != nil {
return err
}
iq.Error = xmppError
continue
}
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
// Decode payload extension
err = d.DecodeElement(iqExt, &tt)
if err != nil {
return err
}
iq.Payload = iqExt
continue
}
// TODO: If unknown decode as generic node
node := new(Node)
err = d.DecodeElement(node, &tt)
if err != nil {
return err
}
iq.Any = node
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
+149
View File
@@ -0,0 +1,149 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Disco Info
const (
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
)
// ----------
// Namespaces
type DiscoInfo struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
Node string `xml:"node,attr,omitempty"`
Identity []Identity `xml:"identity"`
Features []Feature `xml:"feature"`
}
func (d *DiscoInfo) Namespace() string {
return d.XMLName.Space
}
// ---------------
// Builder helpers
// DiscoInfo builds a default DiscoInfo payload
func (iq *IQ) DiscoInfo() *DiscoInfo {
d := DiscoInfo{
XMLName: xml.Name{
Space: NSDiscoInfo,
Local: "query",
},
}
iq.Payload = &d
return &d
}
func (d *DiscoInfo) AddIdentity(name, category, typ string) {
identity := Identity{
XMLName: xml.Name{Local: "identity"},
Name: name,
Category: category,
Type: typ,
}
d.Identity = append(d.Identity, identity)
}
func (d *DiscoInfo) AddFeatures(namespace ...string) {
for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns})
}
}
func (d *DiscoInfo) SetNode(node string) *DiscoInfo {
d.Node = node
return d
}
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
d.Identity = ident
return d
}
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
d.Features = []Feature{}
for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns})
}
return d
}
// -----------
// SubElements
type Identity struct {
XMLName xml.Name `xml:"identity,omitempty"`
Name string `xml:"name,attr,omitempty"`
Category string `xml:"category,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
}
type Feature struct {
XMLName xml.Name `xml:"feature"`
Var string `xml:"var,attr"`
}
// ============================================================================
// Disco Info
const (
NSDiscoItems = "http://jabber.org/protocol/disco#items"
)
type DiscoItems struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
Node string `xml:"node,attr,omitempty"`
Items []DiscoItem `xml:"item"`
}
func (d *DiscoItems) Namespace() string {
return d.XMLName.Space
}
// ---------------
// Builder helpers
// DiscoItems builds a default DiscoItems payload
func (iq *IQ) DiscoItems() *DiscoItems {
d := DiscoItems{
XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", Local: "query"},
}
iq.Payload = &d
return &d
}
func (d *DiscoItems) SetNode(node string) *DiscoItems {
d.Node = node
return d
}
func (d *DiscoItems) AddItem(jid, node, name string) *DiscoItems {
item := DiscoItem{
JID: jid,
Node: node,
Name: name,
}
d.Items = append(d.Items, item)
return d
}
type DiscoItem struct {
XMLName xml.Name `xml:"item"`
JID string `xml:"jid,attr,omitempty"`
Node string `xml:"node,attr,omitempty"`
Name string `xml:"name,attr,omitempty"`
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
}
+90
View File
@@ -0,0 +1,90 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// Test DiscoInfo Builder with several features
func TestDiscoInfo_Builder(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
disco := iq.DiscoInfo()
disco.AddIdentity("Test Component", "gateway", "service")
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check features
features := []string{stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"}
if len(pp.Features) != len(features) {
t.Errorf("Features length mismatch: %#v", pp.Features)
} else {
for i, f := range pp.Features {
if f.Var != features[i] {
t.Errorf("Missing feature: %s", features[i])
}
}
}
// Check identity
if len(pp.Identity) != 1 {
t.Errorf("Identity length mismatch: %#v", pp.Identity)
} else {
if pp.Identity[0].Name != "Test Component" {
t.Errorf("Incorrect identity name: %#v", pp.Identity[0].Name)
}
}
}
// Implements XEP-0030 example 17
// https://xmpp.org/extensions/xep-0030.html#example-17
func TestDiscoItems_Builder(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "catalog.shakespeare.lit",
To: "romeo@montague.net/orchard", Id: "items-2"})
iq.DiscoItems().
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
AddItem("catalog.shakespeare.lit", "music", "Music from the time of Shakespeare")
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoItems)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check items
items := []stanza.DiscoItem{{xml.Name{}, "catalog.shakespeare.lit", "books", "Books by and about Shakespeare"},
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
if len(pp.Items) != len(items) {
t.Errorf("Items length mismatch: %#v", pp.Items)
} else {
for i, item := range pp.Items {
if item.JID != items[i].JID {
t.Errorf("JID Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
if item.Node != items[i].Node {
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
if item.Name != items[i].Name {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
}
}
}
+171
View File
@@ -0,0 +1,171 @@
package stanza_test
import (
"encoding/xml"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
func TestUnmarshalIqs(t *testing.T) {
//var cs1 = new(iot.ControlSet)
var tests = []struct {
iqString string
parsedIQ stanza.IQ
}{
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
stanza.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: stanza.Attrs{Type: stanza.IQTypeSet, To: "test@localhost", Id: "1"}}},
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
}
for _, test := range tests {
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal(%s) returned error", test.iqString)
}
if !xmlEqual(parsedIQ, test.parsedIQ) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ, test.parsedIQ))
}
}
}
func TestGenerateIq(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
payload := stanza.DiscoInfo{
Identity: []stanza.Identity{
{Name: "Test Gateway",
Category: "gateway",
Type: "mqtt",
}},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
},
}
iq.Payload = &payload
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
if strings.Contains(string(data), "<error ") {
t.Error("empty error should not be serialized")
}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
}
}
func TestErrorTag(t *testing.T) {
xError := stanza.Err{
XMLName: xml.Name{Local: "error"},
Code: 503,
Type: "cancel",
Reason: "service-unavailable",
Text: "User session not found",
}
data, err := xml.Marshal(xError)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
}
parsedError := stanza.Err{}
if err = xml.Unmarshal(data, &parsedError); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedError, xError) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedError, xError))
}
}
func TestDiscoItems(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
payload := stanza.DiscoItems{
Node: "music",
}
iq.Payload = &payload
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedIQ.Payload, iq.Payload) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload))
}
}
func TestUnmarshalPayload(t *testing.T) {
query := "<iq to='service.localhost' type='get' id='1'><query xmlns='jabber:iq:version'/></iq>"
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(query), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal(%s) returned error", query)
}
if parsedIQ.Payload == nil {
t.Error("Missing payload")
}
namespace := parsedIQ.Payload.Namespace()
if namespace != "jabber:iq:version" {
t.Errorf("incorrect namespace: %s", namespace)
}
}
func TestPayloadWithError(t *testing.T) {
iq := `<iq xml:lang='en' to='test1@localhost/resource' from='test@localhost' type='error' id='aac1a'>
<query xmlns='jabber:iq:version'/>
<error code='407' type='auth'>
<subscription-required xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
<text xml:lang='en' xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Not subscribed</text>
</error>
</iq>`
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(iq), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal error: %s", iq)
return
}
if parsedIQ.Error.Reason != "subscription-required" {
t.Errorf("incorrect error value: '%s'", parsedIQ.Error.Reason)
}
}
func TestUnknownPayload(t *testing.T) {
iq := `<iq type="get" to="service.localhost" id="1" >
<query xmlns="unknown:ns"/>
</iq>`
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(iq), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal error: %#v (%s)", err, iq)
return
}
if parsedIQ.Any.XMLName.Space != "unknown:ns" {
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
}
}
+45
View File
@@ -0,0 +1,45 @@
package stanza
import "encoding/xml"
// ============================================================================
// Software Version (XEP-0092)
// Version
type Version struct {
XMLName xml.Name `xml:"jabber:iq:version query"`
Name string `xml:"name,omitempty"`
Version string `xml:"version,omitempty"`
OS string `xml:"os,omitempty"`
}
func (v *Version) Namespace() string {
return v.XMLName.Space
}
// ---------------
// Builder helpers
// Version builds a default software version payload
func (iq *IQ) Version() *Version {
d := Version{
XMLName: xml.Name{Space: "jabber:iq:version", Local: "query"},
}
iq.Payload = &d
return &d
}
// Set all software version info
func (v *Version) SetInfo(name, version, os string) *Version {
v.Name = name
v.Version = version
v.OS = os
return v
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
}
+40
View File
@@ -0,0 +1,40 @@
package stanza_test
import (
"testing"
"gosrc.io/xmpp/stanza"
)
// Build a Software Version reply
// https://xmpp.org/extensions/xep-0092.html#example-2
func TestVersion_Builder(t *testing.T) {
name := "Exodus"
version := "0.7.0.4"
os := "Windows-XP 5.01.2600"
iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
To: "juliet@capulet.com/balcony", Id: "version_1"})
iq.Version().SetInfo(name, version, os)
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.Version)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check version info
if pp.Name != name {
t.Errorf("Name Mismatch (expected: %s): %s", name, pp.Name)
}
if pp.Version != version {
t.Errorf("Version Mismatch (expected: %s): %s", version, pp.Version)
}
if pp.OS != os {
t.Errorf("OS Mismatch (expected: %s): %s", os, pp.OS)
}
}
+148
View File
@@ -0,0 +1,148 @@
package stanza
import (
"encoding/xml"
"reflect"
)
// ============================================================================
// Message Packet
// Message implements RFC 6120 - A.5 Client Namespace (a part)
type Message struct {
XMLName xml.Name `xml:"message"`
Attrs
Subject string `xml:"subject,omitempty"`
Body string `xml:"body,omitempty"`
Thread string `xml:"thread,omitempty"`
Error Err `xml:"error,omitempty"`
Extensions []MsgExtension `xml:",omitempty"`
}
func (Message) Name() string {
return "message"
}
func NewMessage(a Attrs) Message {
return Message{
XMLName: xml.Name{Local: "message"},
Attrs: a,
}
}
// Get search and extracts a specific extension on a message.
// It receives a pointer to an MsgExtension. It will panic if the caller
// does not pass a pointer.
// It will return true if the passed extension is found and set the pointer
// to the extension passed as parameter to the found extension.
// It will return false if the extension is not found on the message.
//
// Example usage:
// var oob xmpp.OOB
// if ok := msg.Get(&oob); ok {
// // oob extension has been found
// }
func (msg *Message) Get(ext MsgExtension) bool {
target := reflect.ValueOf(ext)
if target.Kind() != reflect.Ptr {
panic("you must pass a pointer to the message Get method")
}
for _, e := range msg.Extensions {
if reflect.TypeOf(e) == target.Type() {
source := reflect.ValueOf(e)
if source.Kind() != reflect.Ptr {
source = source.Elem()
}
target.Elem().Set(source.Elem())
return true
}
}
return false
}
type messageDecoder struct{}
var message messageDecoder
func (messageDecoder) decode(p *xml.Decoder, se xml.StartElement) (Message, error) {
var packet Message
err := p.DecodeElement(&packet, &se)
return packet, err
}
// XMPPFormat with all Extensions
func (msg *Message) XMPPFormat() string {
out, err := xml.MarshalIndent(msg, "", "")
if err != nil {
return ""
}
return string(out)
}
// UnmarshalXML implements custom parsing for messages
func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
msg.XMLName = start.Name
// Extract packet attributes
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
msg.Id = attr.Value
}
if attr.Name.Local == "type" {
msg.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
msg.To = attr.Value
}
if attr.Name.Local == "from" {
msg.From = attr.Value
}
if attr.Name.Local == "lang" {
msg.Lang = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if msgExt := TypeRegistry.GetMsgExtension(tt.Name); msgExt != nil {
// Decode message extension
err = d.DecodeElement(msgExt, &tt)
if err != nil {
return err
}
msg.Extensions = append(msg.Extensions, msgExt)
} else {
// Decode standard message sub-elements
var err error
switch tt.Name.Local {
case "body":
err = d.DecodeElement(&msg.Body, &tt)
case "thread":
err = d.DecodeElement(&msg.Thread, &tt)
case "subject":
err = d.DecodeElement(&msg.Subject, &tt)
case "error":
err = d.DecodeElement(&msg.Error, &tt)
}
if err != nil {
return err
}
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
+76
View File
@@ -0,0 +1,76 @@
package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
func TestGenerateMessage(t *testing.T) {
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
message.Body = "Hi"
message.Subject = "Msg Subject"
data, err := xml.Marshal(message)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
parsedMessage := stanza.Message{}
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedMessage, message) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedMessage, message))
}
}
func TestDecodeError(t *testing.T) {
str := `<message from='juliet@capulet.com'
id='msg_1'
to='romeo@montague.lit'
type='error'>
<error type='cancel'>
<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</message>`
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message error stanza unmarshall error: %v", err)
return
}
if parsedMessage.Error.Type != "cancel" {
t.Errorf("incorrect error type: %s", parsedMessage.Error.Type)
}
}
func TestGetOOB(t *testing.T) {
image := "https://localhost/image.png"
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
ext := stanza.OOB{
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
URL: image,
}
msg.Extensions = append(msg.Extensions, &ext)
// OOB can properly be found
var oob stanza.OOB
// Try to find and
if ok := msg.Get(&oob); !ok {
t.Error("could not find oob extension")
return
}
if oob.URL != image {
t.Errorf("OOB URL was not properly extracted: ''%s", oob.URL)
}
// Markable is not found
var m stanza.Markable
if ok := msg.Get(&m); ok {
t.Error("we should not have found markable extension")
}
}
+42
View File
@@ -0,0 +1,42 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0333 - Chat Markers: https://xmpp.org/extensions/xep-0333.html
*/
const NSMsgChatMarkers = "urn:xmpp:chat-markers:0"
type Markable struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 markable"`
}
type MarkReceived struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 received"`
ID string `xml:"id,attr"`
}
type MarkDisplayed struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 displayed"`
ID string `xml:"id,attr"`
}
type MarkAcknowledged struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 acknowledged"`
ID string `xml:"id,attr"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{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{})
}
+45
View File
@@ -0,0 +1,45 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0085 - Chat State Notifications: https://xmpp.org/extensions/xep-0085.html
*/
const NSMsgChatStateNotifications = "http://jabber.org/protocol/chatstates"
type StateActive struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates active"`
}
type StateComposing struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates composing"`
}
type StateGone struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates gone"`
}
type StateInactive struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates inactive"`
}
type StatePaused struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates paused"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{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{})
}
+22
View File
@@ -0,0 +1,22 @@
package stanza
import (
"encoding/xml"
)
type HTML struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/xhtml-im html"`
Body HTMLBody
Lang string `xml:"xml:lang,attr,omitempty"`
}
type HTMLBody struct {
XMLName xml.Name `xml:"http://www.w3.org/1999/xhtml body"`
// InnerXML MUST be valid xhtml. We do not check if it is valid when generating the XMPP stanza.
InnerXML string `xml:",innerxml"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{"http://jabber.org/protocol/xhtml-im", "html"}, HTML{})
}
+44
View File
@@ -0,0 +1,44 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestHTMLGen(t *testing.T) {
htmlBody := "<p>Hello <b>World</b></p>"
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
msg.Body = "Hello World"
body := stanza.HTMLBody{
InnerXML: htmlBody,
}
html := stanza.HTML{Body: body}
msg.Extensions = append(msg.Extensions, html)
result := msg.XMPPFormat()
str := `<message to="test@localhost"><body>Hello World</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>Hello <b>World</b></p></body></html></message>`
if result != str {
t.Errorf("incorrect serialize message:\n%s", result)
}
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message HTML unmarshall error: %v", err)
return
}
if parsedMessage.Body != msg.Body {
t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body)
}
var h stanza.HTML
if ok := parsedMessage.Get(&h); !ok {
t.Error("could not extract HTML body")
}
if h.Body.InnerXML != htmlBody {
t.Errorf("could not extract html body: '%s'", h.Body.InnerXML)
}
}
+21
View File
@@ -0,0 +1,21 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0066 - Out of Band Data: https://xmpp.org/extensions/xep-0066.html
*/
type OOB struct {
MsgExtension
XMLName xml.Name `xml:"jabber:x:oob x"`
URL string `xml:"url"`
Desc string `xml:"desc,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{"jabber:x:oob", "x"}, OOB{})
}
+29
View File
@@ -0,0 +1,29 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0184 - Message Delivery Receipts: https://xmpp.org/extensions/xep-0184.html
*/
const NSMsgReceipts = "urn:xmpp:receipts"
// Used on outgoing message, to tell the recipient that you are requesting a message receipt / ack.
type ReceiptRequest struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:receipts request"`
}
type ReceiptReceived struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:receipts received"`
ID string `xml:"id,attr"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "request"}, ReceiptRequest{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "received"}, ReceiptReceived{})
}
+42
View File
@@ -0,0 +1,42 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestDecodeRequest(t *testing.T) {
str := `<message
from='northumberland@shakespeare.lit/westminster'
id='richard2-4.1.247'
to='kingrichard@royalty.england.lit/throne'>
<body>My lord, dispatch; read o'er these articles.</body>
<request xmlns='urn:xmpp:receipts'/>
</message>`
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message receipt unmarshall error: %v", err)
return
}
if parsedMessage.Body != "My lord, dispatch; read o'er these articles." {
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
}
if len(parsedMessage.Extensions) < 1 {
t.Errorf("no extension found on parsed message")
return
}
switch ext := parsedMessage.Extensions[0].(type) {
case *stanza.ReceiptRequest:
if ext.XMLName.Local != "request" {
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
}
default:
t.Errorf("could not find receipts extension")
}
}
+51
View File
@@ -0,0 +1,51 @@
package stanza
import "encoding/xml"
// ============================================================================
// Generic / unknown content
// Node is a generic structure to represent XML data. It is used to parse
// unreferenced or custom stanza payload.
type Node struct {
XMLName xml.Name
Attrs []xml.Attr `xml:"-"`
Content string `xml:",innerxml"`
Nodes []Node `xml:",any"`
}
func (n *Node) Namespace() string {
return n.XMLName.Space
}
// Attr represents generic XML attributes, as used on the generic XML Node
// representation.
type Attr struct {
K string
V string
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
for _, attr := range start.Attr {
// Do not repeat xmlns, it is already in XMLName
if attr.Name.Local != "xmlns" {
n.Attrs = append(n.Attrs, attr)
}
}
type node Node
return d.DecodeElement((*node)(n), &start)
}
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
// Node structure to XML.
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
start.Attr = n.Attrs
start.Name = n.XMLName
err = e.EncodeToken(start)
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
+11
View File
@@ -0,0 +1,11 @@
package stanza
const (
NSStream = "http://etherx.jabber.org/streams"
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
NSClient = "jabber:client"
NSComponent = "jabber:component:accept"
)
+18
View File
@@ -0,0 +1,18 @@
package stanza
type Packet interface {
Name() string
}
// Attrs represents the common structure for base XMPP packets.
type Attrs struct {
Type StanzaType `xml:"type,attr,omitempty"`
Id string `xml:"id,attr,omitempty"`
From string `xml:"from,attr,omitempty"`
To string `xml:"to,attr,omitempty"`
Lang string `xml:"lang,attr,omitempty"`
}
type packetFormatter interface {
XMPPFormat() string
}
+25
View File
@@ -0,0 +1,25 @@
package stanza
type StanzaType string
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
const (
IQTypeError StanzaType = "error"
IQTypeGet StanzaType = "get"
IQTypeResult StanzaType = "result"
IQTypeSet StanzaType = "set"
MessageTypeChat StanzaType = "chat"
MessageTypeError StanzaType = "error"
MessageTypeGroupchat StanzaType = "groupchat"
MessageTypeHeadline StanzaType = "headline"
MessageTypeNormal StanzaType = "normal" // Default
PresenceTypeError StanzaType = "error"
PresenceTypeProbe StanzaType = "probe"
PresenceTypeSubscribe StanzaType = "subscribe"
PresenceTypeSubscribed StanzaType = "subscribed"
PresenceTypeUnavailable StanzaType = "unavailable"
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
)
+153
View File
@@ -0,0 +1,153 @@
package stanza
import (
"encoding/xml"
"errors"
"fmt"
"io"
)
// Reads and checks the opening XMPP stream element.
// TODO It returns a stream structure containing:
// - Host: You can check the host against the host you were expecting to connect to
// - Id: the Stream ID is a temporary shared secret used for some hash calculation. It is also used by ProcessOne
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
// getting through the authentication process.
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
func InitStream(p *xml.Decoder) (sessionID string, err error) {
for {
var t xml.Token
t, err = p.Token()
if err != nil {
return sessionID, err
}
switch elem := t.(type) {
case xml.StartElement:
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
return sessionID, err
}
// Parse XMPP stream attributes
for _, attrs := range elem.Attr {
switch attrs.Name.Local {
case "id":
sessionID = attrs.Value
}
}
return sessionID, err
}
}
}
// NextPacket scans XML token stream for next complete XMPP stanza.
// Once the type of stanza has been identified, a structure is created to decode
// that stanza and returned.
// TODO Use an interface to return packets interface xmppDecoder
// TODO make auth and bind use NextPacket instead of directly NextStart
func NextPacket(p *xml.Decoder) (Packet, error) {
// Read start element to find out how we want to parse the XMPP packet
se, err := NextStart(p)
if err != nil {
return nil, err
}
// Decode one of the top level XMPP namespace
switch se.Name.Space {
case NSStream:
return decodeStream(p, se)
case NSSASL:
return decodeSASL(p, se)
case NSClient:
return decodeClient(p, se)
case NSComponent:
return decodeComponent(p, se)
case NSStreamManagement:
return sm.decode(p, se)
default:
return nil, errors.New("unknown namespace " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// Scan XML token stream to find next StartElement.
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, err := p.Token()
if err == io.EOF {
return xml.StartElement{}, errors.New("connection closed")
}
if err != nil {
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
}
/*
TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that
type.
That way, we have a consistent way to do type assertion, always matching against pointers.
*/
// decodeStream will fully decode a stream packet
func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "error":
return streamError.decode(p, se)
case "features":
return streamFeatures.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// decodeSASL decodes a packet related to SASL authentication.
func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "success":
return saslSuccess.decode(p, se)
case "failure":
return saslFailure.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// decodeClient decodes all known packets in the client namespace.
func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "message":
return message.decode(p, se)
case "presence":
return presence.decode(p, se)
case "iq":
return iq.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// decodeComponent decodes all known packets in the component namespace.
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "handshake": // handshake is used to authenticate components
return handshake.decode(p, se)
case "message":
return message.decode(p, se)
case "presence":
return presence.decode(p, se)
case "iq":
return iq.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
+27
View File
@@ -0,0 +1,27 @@
package stanza
import (
"encoding/xml"
)
type Tune struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/tune tune"`
Artist string `xml:"artist,omitempty"`
Length int `xml:"length,omitempty"`
Rating int `xml:"rating,omitempty"`
Source string `xml:"source,omitempty"`
Title string `xml:"title,omitempty"`
Track string `xml:"track,omitempty"`
Uri string `xml:"uri,omitempty"`
}
// Mood defines deta model for XEP-0107 - User Mood
// See: https://xmpp.org/extensions/xep-0107.html
type Mood struct {
MsgExtension // Mood can be added as a message extension
XMLName xml.Name `xml:"http://jabber.org/protocol/mood mood"`
// TODO: Custom parsing to extract mood type from tag name.
// Note: the list is predefined.
// Mood type
Text string `xml:"text,omitempty"`
}
+148
View File
@@ -0,0 +1,148 @@
package stanza
import (
"encoding/xml"
"strconv"
"time"
)
// ============================================================================
// MUC Presence extension
// MucPresence implements XEP-0045: Multi-User Chat - 19.1
type MucPresence struct {
PresExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/muc x"`
Password string `xml:"password,omitempty"`
History History `xml:"history,omitempty"`
}
const timeLayout = "2006-01-02T15:04:05Z"
// History implements XEP-0045: Multi-User Chat - 19.1
type History struct {
XMLName xml.Name
MaxChars NullableInt `xml:"maxchars,attr,omitempty"`
MaxStanzas NullableInt `xml:"maxstanzas,attr,omitempty"`
Seconds NullableInt `xml:"seconds,attr,omitempty"`
Since time.Time `xml:"since,attr,omitempty"`
}
type NullableInt struct {
Value int
isSet bool
}
func NewNullableInt(val int) NullableInt {
return NullableInt{val, true}
}
func (n NullableInt) Get() (v int, ok bool) {
return n.Value, n.isSet
}
// UnmarshalXML implements custom parsing for history element
func (h *History) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
h.XMLName = start.Name
// Extract attributes
for _, attr := range start.Attr {
switch attr.Name.Local {
case "maxchars":
v, err := strconv.Atoi(attr.Value)
if err != nil {
return err
}
h.MaxChars = NewNullableInt(v)
case "maxstanzas":
v, err := strconv.Atoi(attr.Value)
if err != nil {
return err
}
h.MaxStanzas = NewNullableInt(v)
case "seconds":
v, err := strconv.Atoi(attr.Value)
if err != nil {
return err
}
h.Seconds = NewNullableInt(v)
case "since":
t, err := time.Parse(timeLayout, attr.Value)
if err != nil {
return err
}
h.Since = t
}
}
// Consume remaining data until element end
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
mc, isMcSet := h.MaxChars.Get()
ms, isMsSet := h.MaxStanzas.Get()
s, isSSet := h.Seconds.Get()
// We do not have any value, ignore history element
if h.Since.IsZero() && !isMcSet && !isMsSet && !isSSet {
return nil
}
// Encode start element and attributes
start.Name = xml.Name{Local: "history"}
if isMcSet {
attr := xml.Attr{
Name: xml.Name{Local: "maxchars"},
Value: strconv.Itoa(mc),
}
start.Attr = append(start.Attr, attr)
}
if isMsSet {
attr := xml.Attr{
Name: xml.Name{Local: "maxstanzas"},
Value: strconv.Itoa(ms),
}
start.Attr = append(start.Attr, attr)
}
if isSSet {
attr := xml.Attr{
Name: xml.Name{Local: "seconds"},
Value: strconv.Itoa(s),
}
start.Attr = append(start.Attr, attr)
}
if !h.Since.IsZero() {
attr := xml.Attr{
Name: xml.Name{Local: "since"},
Value: h.Since.Format(timeLayout),
}
start.Attr = append(start.Attr, attr)
}
if err := e.EncodeToken(start); err != nil {
return err
}
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
func init() {
TypeRegistry.MapExtension(PKTPresence, xml.Name{"http://jabber.org/protocol/muc", "x"}, MucPresence{})
}
+97
View File
@@ -0,0 +1,97 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// https://xmpp.org/extensions/xep-0045.html#example-27
func TestMucPassword(t *testing.T) {
str := `<presence
from='hag66@shakespeare.lit/pda'
id='djn4714'
to='coven@chat.shakespeare.lit/thirdwitch'>
<x xmlns='http://jabber.org/protocol/muc'>
<password>cauldronburn</password>
</x>
</presence>`
var parsedPresence stanza.Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", str)
}
var muc stanza.MucPresence
if ok := parsedPresence.Get(&muc); !ok {
t.Error("muc presence extension was not found")
}
if muc.Password != "cauldronburn" {
t.Errorf("incorrect password: '%s'", muc.Password)
}
}
// https://xmpp.org/extensions/xep-0045.html#example-37
func TestMucHistory(t *testing.T) {
str := `<presence
from='hag66@shakespeare.lit/pda'
id='n13mt3l'
to='coven@chat.shakespeare.lit/thirdwitch'>
<x xmlns='http://jabber.org/protocol/muc'>
<history maxstanzas='20'/>
</x>
</presence>`
var parsedPresence stanza.Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", str, err)
return
}
var muc stanza.MucPresence
if ok := parsedPresence.Get(&muc); !ok {
t.Error("muc presence extension was not found")
return
}
if v, ok := muc.History.MaxStanzas.Get(); !ok || v != 20 {
t.Errorf("incorrect MaxStanzas: '%#v'", muc.History.MaxStanzas)
}
}
// https://xmpp.org/extensions/xep-0045.html#example-37
func TestMucNoHistory(t *testing.T) {
str := "<presence" +
" id=\"n13mt3l\"" +
" from=\"hag66@shakespeare.lit/pda\"" +
" to=\"coven@chat.shakespeare.lit/thirdwitch\">" +
"<x xmlns=\"http://jabber.org/protocol/muc\">" +
"<history maxstanzas=\"0\"></history>" +
"</x>" +
"</presence>"
maxstanzas := 0
pres := stanza.Presence{Attrs: stanza.Attrs{
From: "hag66@shakespeare.lit/pda",
Id: "n13mt3l",
To: "coven@chat.shakespeare.lit/thirdwitch",
},
Extensions: []stanza.PresExtension{
stanza.MucPresence{
History: stanza.History{MaxStanzas: stanza.NewNullableInt(maxstanzas)},
},
},
}
data, err := xml.Marshal(&pres)
if err != nil {
t.Error("error on encode:", err)
return
}
if string(data) != str {
t.Errorf("incorrect stanza: \n%s\n%s", str, data)
}
}
+139
View File
@@ -0,0 +1,139 @@
package stanza
import (
"encoding/xml"
"reflect"
)
// ============================================================================
// Presence Packet
// Presence implements RFC 6120 - A.5 Client Namespace (a part)
type Presence struct {
XMLName xml.Name `xml:"presence"`
Attrs
Show PresenceShow `xml:"show,omitempty"`
Status string `xml:"status,omitempty"`
Priority int8 `xml:"priority,omitempty"` // default: 0
Error Err `xml:"error,omitempty"`
Extensions []PresExtension `xml:",omitempty"`
}
func (Presence) Name() string {
return "presence"
}
func NewPresence(a Attrs) Presence {
return Presence{
XMLName: xml.Name{Local: "presence"},
Attrs: a,
}
}
// Get search and extracts a specific extension on a presence stanza.
// It receives a pointer to an PresExtension. It will panic if the caller
// does not pass a pointer.
// It will return true if the passed extension is found and set the pointer
// to the extension passed as parameter to the found extension.
// It will return false if the extension is not found on the presence.
//
// Example usage:
// var muc xmpp.MucPresence
// if ok := msg.Get(&muc); ok {
// // muc presence extension has been found
// }
func (pres *Presence) Get(ext PresExtension) bool {
target := reflect.ValueOf(ext)
if target.Kind() != reflect.Ptr {
panic("you must pass a pointer to the message Get method")
}
for _, e := range pres.Extensions {
if reflect.TypeOf(e) == target.Type() {
source := reflect.ValueOf(e)
if source.Kind() != reflect.Ptr {
source = source.Elem()
}
target.Elem().Set(source.Elem())
return true
}
}
return false
}
type presenceDecoder struct{}
var presence presenceDecoder
func (presenceDecoder) decode(p *xml.Decoder, se xml.StartElement) (Presence, error) {
var packet Presence
err := p.DecodeElement(&packet, &se)
// TODO Add default presence type (when omitted)
return packet, err
}
// UnmarshalXML implements custom parsing for presence stanza
func (pres *Presence) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
pres.XMLName = start.Name
// Extract packet attributes
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
pres.Id = attr.Value
}
if attr.Name.Local == "type" {
pres.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
pres.To = attr.Value
}
if attr.Name.Local == "from" {
pres.From = attr.Value
}
if attr.Name.Local == "lang" {
pres.Lang = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if presExt := TypeRegistry.GetPresExtension(tt.Name); presExt != nil {
// Decode message extension
err = d.DecodeElement(presExt, &tt)
if err != nil {
return err
}
pres.Extensions = append(pres.Extensions, presExt)
} else {
// Decode standard message sub-elements
var err error
switch tt.Name.Local {
case "show":
err = d.DecodeElement(&pres.Show, &tt)
case "status":
err = d.DecodeElement(&pres.Status, &tt)
case "priority":
err = d.DecodeElement(&pres.Priority, &tt)
case "error":
err = d.DecodeElement(&pres.Error, &tt)
}
if err != nil {
return err
}
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
package stanza
// PresenceShow is a Enum of presence element show
type PresenceShow string
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
const (
PresenceShowAway PresenceShow = "away"
PresenceShowChat PresenceShow = "chat"
PresenceShowDND PresenceShow = "dnd"
PresenceShowXA PresenceShow = "xa"
)
+63
View File
@@ -0,0 +1,63 @@
package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
func TestGeneratePresence(t *testing.T) {
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = stanza.PresenceShowChat
data, err := xml.Marshal(presence)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
var parsedPresence stanza.Presence
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedPresence, presence) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedPresence, presence))
}
}
func TestPresenceSubElt(t *testing.T) {
// Test structure to ensure that show, status and priority are correctly defined as presence
// package sub-elements
type pres struct {
Show stanza.PresenceShow `xml:"show"`
Status string `xml:"status"`
Priority int8 `xml:"priority"`
}
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = stanza.PresenceShowXA
presence.Status = "Coding"
presence.Priority = 10
data, err := xml.Marshal(presence)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
var parsedPresence pres
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if parsedPresence.Show != presence.Show {
t.Errorf("cannot read 'show' as presence subelement (%s)", parsedPresence.Show)
}
if parsedPresence.Status != presence.Status {
t.Errorf("cannot read 'status' as presence subelement (%s)", parsedPresence.Status)
}
if parsedPresence.Priority != presence.Priority {
t.Errorf("cannot read 'priority' as presence subelement (%d)", parsedPresence.Priority)
}
}
+40
View File
@@ -0,0 +1,40 @@
package stanza
import (
"encoding/xml"
)
type PubSub struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
Publish *Publish
Retract *Retract
// TODO <configure/>
}
func (p *PubSub) Namespace() string {
return p.XMLName.Space
}
type Publish struct {
XMLName xml.Name `xml:"publish"`
Node string `xml:"node,attr"`
Item Item
}
type Item struct {
XMLName xml.Name `xml:"item"`
Id string `xml:"id,attr,omitempty"`
Tune *Tune
Mood *Mood
}
type Retract struct {
XMLName xml.Name `xml:"retract"`
Node string `xml:"node,attr"`
Notify string `xml:"notify,attr"`
Item Item
}
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{})
}
+119
View File
@@ -0,0 +1,119 @@
package stanza
import (
"encoding/xml"
"reflect"
"sync"
)
type MsgExtension interface{}
type PresExtension interface{}
// The Registry for msg and IQ types is a global variable.
// TODO: Move to the client init process to remove the dependency on a global variable.
// That should make it possible to be able to share the decoder.
// TODO: Ensure that a client can add its own custom namespace to the registry (or overload existing ones).
type PacketType uint8
const (
PKTPresence PacketType = iota
PKTMessage
PKTIQ
)
var TypeRegistry = newRegistry()
// We store different registries per packet type and namespace.
type registryKey struct {
packetType PacketType
namespace string
}
type registryForNamespace map[string]reflect.Type
type registry struct {
// We store different registries per packet type and namespace.
msgTypes map[registryKey]registryForNamespace
// Handle concurrent access
msgTypesLock *sync.RWMutex
}
func newRegistry() *registry {
return &registry{
msgTypes: make(map[registryKey]registryForNamespace),
msgTypesLock: &sync.RWMutex{},
}
}
// MapExtension stores extension type for packet payload.
// The match is done per PacketType (iq, message, or presence) and XML tag name.
// You can use the alias "*" as local XML name to be able to match all unknown tag name for that
// packet type and namespace.
func (r *registry) MapExtension(pktType PacketType, name xml.Name, extension MsgExtension) {
key := registryKey{pktType, name.Space}
r.msgTypesLock.RLock()
store := r.msgTypes[key]
r.msgTypesLock.RUnlock()
r.msgTypesLock.Lock()
defer r.msgTypesLock.Unlock()
if store == nil {
store = make(map[string]reflect.Type)
}
store[name.Local] = reflect.TypeOf(extension)
r.msgTypes[key] = store
}
// GetExtensionType returns extension type for packet payload, based on packet type and tag name.
func (r *registry) GetExtensionType(pktType PacketType, name xml.Name) reflect.Type {
key := registryKey{pktType, name.Space}
r.msgTypesLock.RLock()
defer r.msgTypesLock.RUnlock()
store := r.msgTypes[key]
result := store[name.Local]
if result == nil && name.Local != "*" {
return store["*"]
}
return result
}
// GetPresExtension returns an instance of PresExtension, by matching packet type and XML
// tag name against the registry.
func (r *registry) GetPresExtension(name xml.Name) PresExtension {
if extensionType := r.GetExtensionType(PKTPresence, name); extensionType != nil {
val := reflect.New(extensionType)
elt := val.Interface()
if presExt, ok := elt.(PresExtension); ok {
return presExt
}
}
return nil
}
// GetMsgExtension returns an instance of MsgExtension, by matching packet type and XML
// tag name against the registry.
func (r *registry) GetMsgExtension(name xml.Name) MsgExtension {
if extensionType := r.GetExtensionType(PKTMessage, name); extensionType != nil {
val := reflect.New(extensionType)
elt := val.Interface()
if msgExt, ok := elt.(MsgExtension); ok {
return msgExt
}
}
return nil
}
// GetIQExtension returns an instance of IQPayload, by matching packet type and XML
// tag name against the registry.
func (r *registry) GetIQExtension(name xml.Name) IQPayload {
if extensionType := r.GetExtensionType(PKTIQ, name); extensionType != nil {
val := reflect.New(extensionType)
elt := val.Interface()
if iqExt, ok := elt.(IQPayload); ok {
return iqExt
}
}
return nil
}
+47
View File
@@ -0,0 +1,47 @@
package stanza
import (
"encoding/xml"
"reflect"
"testing"
)
func TestRegistry_RegisterMsgExt(t *testing.T) {
// Setup registry
typeRegistry := newRegistry()
// Register an element
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
// Match that element
receipt := typeRegistry.GetMsgExtension(name)
if receipt == nil {
t.Error("cannot read element type from registry")
return
}
switch r := receipt.(type) {
case *ReceiptRequest:
default:
t.Errorf("Registry did not return expected type ReceiptRequest: %v", reflect.TypeOf(r))
}
}
func BenchmarkRegistryGet(b *testing.B) {
// Setup registry
typeRegistry := newRegistry()
// Register an element
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
for i := 0; i < b.N; i++ {
// Match that element
receipt := typeRegistry.GetExtensionType(PKTMessage, name)
if receipt == nil {
b.Error("cannot read element type from registry")
return
}
}
}
+112
View File
@@ -0,0 +1,112 @@
package stanza
import "encoding/xml"
// ============================================================================
// SASLAuth implements SASL Authentication initiation.
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.2
type SASLAuth struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
Mechanism string `xml:"mechanism,attr"`
Value string `xml:",innerxml"`
}
// ============================================================================
// SASLSuccess implements SASL Success nonza, sent by server as a result of the
// SASL auth negotiation.
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.6
type SASLSuccess struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
}
func (SASLSuccess) Name() string {
return "sasl:success"
}
// SASLSuccess decoding
type saslSuccessDecoder struct{}
var saslSuccess saslSuccessDecoder
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
var packet SASLSuccess
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// SASLFailure
type SASLFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
Any xml.Name // error reason is a subelement
}
func (SASLFailure) Name() string {
return "sasl:failure"
}
// SASLFailure decoding
type saslFailureDecoder struct{}
var saslFailure saslFailureDecoder
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
var packet SASLFailure
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ===========================================================================
// Resource binding
// Bind is an IQ payload used during session negotiation to bind user resource
// to the current XMPP stream.
// Reference: https://tools.ietf.org/html/rfc6120#section-7
type Bind struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource string `xml:"resource,omitempty"`
Jid string `xml:"jid,omitempty"`
}
func (b *Bind) Namespace() string {
return b.XMLName.Space
}
// ============================================================================
// Session (Obsolete)
// Session is both a stream feature and an obsolete IQ Payload, used to bind a
// resource to the current XMPP stream on RFC 3121 only XMPP servers.
// Session is obsolete in RFC 6121. It is added to Fluux XMPP for compliance
// with RFC 3121.
// Reference: https://xmpp.org/rfcs/rfc3921.html#session
//
// This is the draft defining how to handle the transition:
// https://tools.ietf.org/html/draft-cridland-xmpp-session-01
type StreamSession struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
Optional bool // If element does exist, it mean we are not required to open session
}
func (s *StreamSession) Namespace() string {
return s.XMLName.Space
}
func (s *StreamSession) IsOptional() bool {
if s.XMLName.Local == "session" {
return s.Optional
}
// If session element is missing, then we should not use session
return true
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, Bind{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-session", "session"}, StreamSession{})
}
+57
View File
@@ -0,0 +1,57 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// Check that we can detect optional session from advertised stream features
func TestSessionFeatures(t *testing.T) {
streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: true}}
data, err := xml.Marshal(streamFeatures)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
}
parsedStream := stanza.StreamFeatures{}
if err = xml.Unmarshal(data, &parsedStream); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
}
if !parsedStream.Session.IsOptional() {
t.Error("Session should be optional")
}
}
// Check that the Session tag can be used in IQ decoding
func TestSessionIQ(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"})
iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: true}
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
return
}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
return
}
session, ok := parsedIQ.Payload.(*stanza.StreamSession)
if !ok {
t.Error("Missing session payload")
return
}
if !session.IsOptional() {
t.Error("Session should be optional")
}
}
// TODO Test Sasl mechanism
+14
View File
@@ -0,0 +1,14 @@
package stanza
import (
"encoding/xml"
)
// Used during stream initiation / session establishment
type TLSProceed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
}
type tlsFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
}
+167
View File
@@ -0,0 +1,167 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// StreamFeatures Packet
// Reference: The active stream features are published on
// https://xmpp.org/registrar/stream-features.html
// Note: That page misses draft and experimental XEP (i.e CSI, etc)
type StreamFeatures struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
// Server capabilities hash
Caps Caps
// Stream features
StartTLS tlsStartTLS
Mechanisms saslMechanisms
Bind Bind
StreamManagement streamManagement
// Obsolete
Session StreamSession
// ProcessOne Stream Features
P1Push p1Push
P1Rebind p1Rebind
p1Ack p1Ack
Any []xml.Name `xml:",any"`
}
func (StreamFeatures) Name() string {
return "stream:features"
}
type streamFeatureDecoder struct{}
var streamFeatures streamFeatureDecoder
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
var packet StreamFeatures
err := p.DecodeElement(&packet, &se)
return packet, err
}
// Capabilities
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
// and peer servers do not need to send service discovery requests each time they connect."
// This is not a stream feature but a way to let client cache server disco info.
type Caps struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
Hash string `xml:"hash,attr"`
Node string `xml:"node,attr"`
Ver string `xml:"ver,attr"`
Ext string `xml:"ext,attr,omitempty"`
}
// ============================================================================
// Supported Stream Features
// StartTLS feature
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
type tlsStartTLS struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
Required bool
}
// UnmarshalXML implements custom parsing startTLS required flag
func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
stls.XMLName = start.Name
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
elt := new(Node)
err = d.DecodeElement(elt, &tt)
if err != nil {
return err
}
if elt.XMLName.Local == "required" {
stls.Required = true
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (sf *StreamFeatures) DoesStartTLS() (feature tlsStartTLS, isSupported bool) {
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
return sf.StartTLS, true
}
return feature, false
}
// Mechanisms
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
type saslMechanisms struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
Mechanism []string `xml:"mechanism"`
}
// StreamManagement
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
type streamManagement struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
}
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
return true
}
return false
}
// P1 extensions
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
// p1:push support
type p1Push struct {
XMLName xml.Name `xml:"p1:push push"`
}
// p1:rebind suppor
type p1Rebind struct {
XMLName xml.Name `xml:"p1:rebind rebind"`
}
// p1:ack support
type p1Ack struct {
XMLName xml.Name `xml:"p1:ack ack"`
}
// ============================================================================
// StreamError Packet
type StreamError struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
Error xml.Name `xml:",any"`
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
}
func (StreamError) Name() string {
return "stream:error"
}
type streamErrorDecoder struct{}
var streamError streamErrorDecoder
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
var packet StreamError
err := p.DecodeElement(&packet, &se)
return packet, err
}
+121
View File
@@ -0,0 +1,121 @@
package stanza
import (
"encoding/xml"
"errors"
)
const (
NSStreamManagement = "urn:xmpp:sm:3"
)
// Enabled as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#enable
type SMEnabled struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 enabled"`
Id string `xml:"id,attr,omitempty"`
Location string `xml:"location,attr,omitempty"`
Resume string `xml:"resume,attr,omitempty"`
Max uint `xml:"max,attr,omitempty"`
}
func (SMEnabled) Name() string {
return "Stream Management: enabled"
}
// Request as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMRequest struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 r"`
}
func (SMRequest) Name() string {
return "Stream Management: request"
}
// Answer as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMAnswer struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 a"`
H uint `xml:"h,attr,omitempty"`
}
func (SMAnswer) Name() string {
return "Stream Management: answer"
}
// Resumed as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMResumed struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"`
PrevId string `xml:"previd,attr,omitempty"`
H uint `xml:"h,attr,omitempty"`
}
func (SMResumed) Name() string {
return "Stream Management: resumed"
}
// Failed as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMFailed struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 failed"`
// TODO: Handle decoding error cause (need custom parsing).
}
func (SMFailed) Name() string {
return "Stream Management: failed"
}
type smDecoder struct{}
var sm smDecoder
// decode decodes all known nonza in the stream management namespace.
func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "enabled":
return s.decodeEnabled(p, se)
case "resumed":
return s.decodeResumed(p, se)
case "r":
return s.decodeRequest(p, se)
case "h":
return s.decodeAnswer(p, se)
case "failed":
return s.decodeFailed(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
func (smDecoder) decodeEnabled(p *xml.Decoder, se xml.StartElement) (SMEnabled, error) {
var packet SMEnabled
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed, error) {
var packet SMResumed
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeRequest(p *xml.Decoder, se xml.StartElement) (SMRequest, error) {
var packet SMRequest
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeAnswer(p *xml.Decoder, se xml.StartElement) (SMAnswer, error) {
var packet SMAnswer
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeFailed(p *xml.Decoder, se xml.StartElement) (SMFailed, error) {
var packet SMFailed
err := p.DecodeElement(&packet, &se)
return packet, err
}

Some files were not shown because too many files have changed in this diff Show More