mirror of
https://github.com/FluuxIO/go-xmpp.git
synced 2025-11-06 18:23:43 -08:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8794ea6ed8 | ||
|
|
7e596fc33c | ||
|
|
6f9808fe16 | ||
|
|
7b1f83f6b7 | ||
|
|
6005a964ba | ||
|
|
d3b45b42a5 | ||
|
|
6e65ba2a0b | ||
|
|
76f59be5ed | ||
|
|
80d2e0fa1e | ||
|
|
2e864ff7f6 | ||
|
|
3de99e0e0e | ||
|
|
e531370dc9 | ||
|
|
4e185f4bb6 | ||
|
|
4f1e0ded97 | ||
|
|
176dcdce33 | ||
|
|
61adf7e414 | ||
|
|
014957e029 | ||
|
|
69118a952a | ||
|
|
1c74d102c7 | ||
|
|
7ab6c3a62d | ||
|
|
a3867dd0b3 | ||
|
|
d2a1329dc6 | ||
|
|
6ff7812ac4 | ||
|
|
3453336f27 | ||
|
|
a23194ad96 | ||
|
|
f984a93e63 | ||
|
|
6a5f2750f1 | ||
|
|
e553028754 | ||
|
|
fed23ad7ad | ||
|
|
244acdc02a | ||
|
|
4d6c783619 | ||
|
|
5697d40e5c | ||
|
|
ff5885f29d | ||
|
|
e3e57ac803 | ||
|
|
3daa5c505c | ||
|
|
0fb90abcf7 | ||
|
|
6aa942dd58 | ||
|
|
c41d068c9f | ||
|
|
9f095cb90f | ||
|
|
7deaf59642 | ||
|
|
fe6cea870d | ||
|
|
323de704f6 | ||
|
|
e05f36c69f | ||
|
|
d36428fb2f | ||
|
|
9577036327 | ||
|
|
79803a8af9 | ||
|
|
604d2c6c1e | ||
|
|
7c71d93026 | ||
|
|
cca0919b8a | ||
|
|
40e907e8ee | ||
|
|
838c059398 | ||
|
|
3ba59afd6e | ||
|
|
661188752e | ||
|
|
409d563eec | ||
|
|
d90cc239ae | ||
|
|
b35868b689 | ||
|
|
6165232d7a | ||
|
|
91c562200d | ||
|
|
5992cc2231 | ||
|
|
318e5e8a50 | ||
|
|
a465e370e2 | ||
|
|
9bb4f32769 | ||
|
|
e3c0747cbb | ||
|
|
0fd1bb2483 | ||
|
|
4a4fc39cf6 | ||
|
|
5db9a80605 | ||
|
|
20a66dc47d | ||
|
|
1dacc663d3 | ||
|
|
cb9016693c | ||
|
|
0c7e4588c6 | ||
|
|
3fa1a4b387 | ||
|
|
80f32b4af7 | ||
|
|
781b875cf1 | ||
|
|
3d088a6078 | ||
|
|
0ee4764d31 | ||
|
|
1971772394 | ||
|
|
6fbfe9fd0a | ||
|
|
5ed66de79e | ||
|
|
428787d7ab | ||
|
|
0acf824217 | ||
|
|
445bb8efa3 | ||
|
|
f79a3a219b | ||
|
|
1c792e61c6 | ||
|
|
fde524ef98 | ||
|
|
7a386ec8d0 | ||
|
|
83f96fbd41 | ||
|
|
def9629a0b | ||
|
|
1542110f1b | ||
|
|
d6d371df4d | ||
|
|
3521c488ea | ||
|
|
8f7b4ba8a4 | ||
|
|
4a4c4850d1 | ||
|
|
6ddfa781e5 | ||
|
|
555cbe12b4 | ||
|
|
e9c704eff5 | ||
|
|
d9fdff0839 | ||
|
|
145fce6b3f | ||
|
|
5d362b505b | ||
|
|
923fd61587 | ||
|
|
44681e8053 | ||
|
|
1a7aa94bae | ||
|
|
a6cbc0c08f | ||
|
|
3f81465c6c | ||
|
|
24502f7cd7 | ||
|
|
af0ae525b8 | ||
|
|
d455f29258 | ||
|
|
683fdea2ec | ||
|
|
7f889909fd | ||
|
|
4d015e5b29 | ||
|
|
c8ded1462f | ||
|
|
28ae759144 | ||
|
|
55c7251fac | ||
|
|
398ba224e7 | ||
|
|
00e9dd4e47 | ||
|
|
ddff6527bd | ||
|
|
9219bf5aa9 | ||
|
|
715bf6976f | ||
|
|
348f29e055 | ||
|
|
45c7ca74b1 | ||
|
|
7aef8357ed | ||
|
|
2c7b03fcea | ||
|
|
9b57809e9d | ||
|
|
f0f0d5a285 | ||
|
|
61cdac89e0 | ||
|
|
c6f0d03f60 | ||
|
|
cc2fa7307f | ||
|
|
9db33d5792 | ||
|
|
b05e68c844 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -31,4 +31,6 @@ coverage.txt
|
||||
|
||||
# Do not commit codeship key
|
||||
codeship.aes
|
||||
codeship.env
|
||||
codeship.env
|
||||
|
||||
priv/
|
||||
110
README.md
110
README.md
@@ -8,14 +8,77 @@ The goal is to make simple to write simple XMPP clients and components:
|
||||
|
||||
- For automation (like for example monitoring of an XMPP service),
|
||||
- For building connected "things" by plugging them on an XMPP server,
|
||||
- For writing simple chatbot to control a service or a thing.
|
||||
- For writing XMPP servers components (See [XEP-0114](https://xmpp.org/extensions/xep-0114.html))
|
||||
- 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.
|
||||
|
||||
## Example
|
||||
## Configuration and connection
|
||||
|
||||
Here is a demo "echo" client:
|
||||
### 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
|
||||
@@ -26,6 +89,7 @@ import (
|
||||
"os"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -33,37 +97,37 @@ func main() {
|
||||
Address: "localhost:5222",
|
||||
Jid: "test@localhost",
|
||||
Password: "test",
|
||||
PacketLogger: os.Stdout,
|
||||
StreamLogger: os.Stdout,
|
||||
Insecure: true,
|
||||
}
|
||||
|
||||
client, err := xmpp.NewClient(config)
|
||||
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.NewClientManager(client, nil)
|
||||
err = cm.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
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
|
||||
}
|
||||
|
||||
// Iterator to receive packets coming from our XMPP connection
|
||||
for packet := range client.Recv() {
|
||||
switch packet := packet.(type) {
|
||||
case xmpp.Message:
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
|
||||
reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body}
|
||||
_ = client.Send(reply)
|
||||
default:
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
||||
}
|
||||
}
|
||||
_, _ = 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)
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## Reference documentation
|
||||
|
||||
Please, check GoDoc for more information: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
|
||||
The code documentation is available on GoDoc: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
|
||||
|
||||
5
_examples/custom_stanza/README.md
Normal file
5
_examples/custom_stanza/README.md
Normal 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
_examples/custom_stanza/custom_stanza.go
Normal file
49
_examples/custom_stanza/custom_stanza.go
Normal 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
_examples/delegation/README.md
Normal file
5
_examples/delegation/README.md
Normal 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
_examples/delegation/delegation.go
Normal file
206
_examples/delegation/delegation.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@ module gosrc.io/xmpp/_examples
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.3.0 // indirect
|
||||
github.com/processone/mpg123 v1.0.0
|
||||
github.com/processone/soundcloud v1.0.0
|
||||
gosrc.io/xmpp v0.0.0-20190611132908-4d4710463dbc
|
||||
gosrc.io/xmpp v0.1.1
|
||||
)
|
||||
|
||||
replace gosrc.io/xmpp => ./../
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
||||
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gosrc.io/xmpp v0.0.0-20190611132908-4d4710463dbc h1:+XtYQ6hbNiPehZdPz3SU049S1wFFa4KKZxDtGITvyW8=
|
||||
gosrc.io/xmpp v0.0.0-20190611132908-4d4710463dbc/go.mod h1:6NJG4vRCxQJMGLxIdroPLPd++FPLOmDqJdJEt2mu4kQ=
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
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:
|
||||
|
||||
@@ -5,118 +5,97 @@ import (
|
||||
"log"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func main() {
|
||||
opts := xmpp.ComponentOptions{
|
||||
Domain: "service.localhost",
|
||||
Domain: "service2.localhost",
|
||||
Secret: "mypass",
|
||||
Address: "localhost:8888",
|
||||
Name: "Test Component",
|
||||
Category: "gateway",
|
||||
Type: "service",
|
||||
}
|
||||
component, err := xmpp.NewComponent(opts)
|
||||
|
||||
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 connection manager, it will handle the reconnect policy
|
||||
// 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)
|
||||
err = cm.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Iterator to receive packets coming from our XMPP connection
|
||||
for packet := range component.Recv() {
|
||||
switch p := packet.(type) {
|
||||
case xmpp.IQ:
|
||||
switch inner := p.Payload[0].(type) {
|
||||
case *xmpp.DiscoInfo:
|
||||
fmt.Println("DiscoInfo")
|
||||
if p.Type == "get" {
|
||||
discoResult(component, p.PacketAttrs, inner)
|
||||
}
|
||||
case *xmpp.DiscoItems:
|
||||
fmt.Println("DiscoItems")
|
||||
if p.Type == "get" {
|
||||
discoItems(component, p.PacketAttrs, inner)
|
||||
}
|
||||
case *xmpp.Version:
|
||||
fmt.Println("Version")
|
||||
if p.Type == "get" {
|
||||
version(component, p.PacketAttrs)
|
||||
}
|
||||
default:
|
||||
fmt.Println("ignoring iq packet", inner)
|
||||
xError := xmpp.Err{
|
||||
Code: 501,
|
||||
Reason: "feature-not-implemented",
|
||||
Type: "cancel",
|
||||
}
|
||||
reply := p.MakeError(xError)
|
||||
_ = component.Send(&reply)
|
||||
}
|
||||
|
||||
case xmpp.Message:
|
||||
fmt.Println("Received message:", p.Body)
|
||||
|
||||
case xmpp.Presence:
|
||||
fmt.Println("Received presence:", p.Type)
|
||||
|
||||
default:
|
||||
fmt.Println("ignoring packet:", packet)
|
||||
}
|
||||
}
|
||||
log.Fatal(cm.Run())
|
||||
}
|
||||
|
||||
func discoResult(c *xmpp.Component, attrs xmpp.PacketAttrs, info *xmpp.DiscoInfo) {
|
||||
iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
|
||||
var identity xmpp.Identity
|
||||
if info.Node == "" {
|
||||
identity = xmpp.Identity{
|
||||
Name: c.Name,
|
||||
Category: c.Category,
|
||||
Type: c.Type,
|
||||
}
|
||||
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||
msg, ok := p.(stanza.Message)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
payload := xmpp.DiscoInfo{
|
||||
Identity: identity,
|
||||
Features: []xmpp.Feature{
|
||||
{Var: xmpp.NSDiscoInfo},
|
||||
{Var: xmpp.NSDiscoItems},
|
||||
{Var: "jabber:iq:version"},
|
||||
},
|
||||
}
|
||||
iq.AddPayload(&payload)
|
||||
|
||||
_ = c.Send(iq)
|
||||
fmt.Println("Received message:", msg.Body)
|
||||
}
|
||||
|
||||
func discoItems(c *xmpp.Component, attrs xmpp.PacketAttrs, items *xmpp.DiscoItems) {
|
||||
iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
|
||||
|
||||
var payload xmpp.DiscoItems
|
||||
if items.Node == "" {
|
||||
payload = xmpp.DiscoItems{
|
||||
Items: []xmpp.DiscoItem{
|
||||
{Name: "test node", JID: "service.localhost", Node: "node1"},
|
||||
},
|
||||
}
|
||||
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
|
||||
}
|
||||
iq.AddPayload(&payload)
|
||||
_ = c.Send(iq)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func version(c *xmpp.Component, attrs xmpp.PacketAttrs) {
|
||||
iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
|
||||
// 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
|
||||
}
|
||||
|
||||
var payload xmpp.Version
|
||||
payload.Name = "Fluux XMPP Component"
|
||||
payload.Version = "0.0.1"
|
||||
iq.AddPayload(&payload)
|
||||
_ = c.Send(iq)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -17,11 +18,15 @@ func main() {
|
||||
Address: "localhost:5222",
|
||||
Jid: "test@localhost",
|
||||
Password: "test",
|
||||
PacketLogger: os.Stdout,
|
||||
StreamLogger: os.Stdout,
|
||||
Insecure: true,
|
||||
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client, err := xmpp.NewClient(config)
|
||||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
@@ -29,22 +34,19 @@ func main() {
|
||||
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||
// for you automatically.
|
||||
cm := xmpp.NewStreamManager(client, nil)
|
||||
err = cm.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
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
|
||||
}
|
||||
|
||||
// Iterator to receive packets coming from our XMPP connection
|
||||
for packet := range client.Recv() {
|
||||
switch packet := packet.(type) {
|
||||
case xmpp.Message:
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
|
||||
reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body}
|
||||
_ = client.Send(reply)
|
||||
default:
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
||||
}
|
||||
}
|
||||
_, _ = 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,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"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.
|
||||
@@ -23,47 +24,64 @@ func main() {
|
||||
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
|
||||
flag.Parse()
|
||||
|
||||
var client *xmpp.Client
|
||||
var err error
|
||||
if client, err = connectXmpp(*jid, *password, *address); err != nil {
|
||||
log.Fatal("Could not connect to XMPP: ", err)
|
||||
}
|
||||
|
||||
p, err := mpg123.NewPlayer()
|
||||
// 1. Create mpg player
|
||||
player, err := mpg123.NewPlayer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Iterator to receive packets coming from our XMPP connection
|
||||
for packet := range client.Recv() {
|
||||
|
||||
switch packet := packet.(type) {
|
||||
case xmpp.Message:
|
||||
processMessage(client, p, &packet)
|
||||
case xmpp.IQ:
|
||||
processIq(client, p, &packet)
|
||||
case xmpp.Presence:
|
||||
// Do nothing with received presence
|
||||
default:
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
||||
}
|
||||
// 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 processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.Message) {
|
||||
command := strings.Trim(packet.Body, " ")
|
||||
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" {
|
||||
p.Stop()
|
||||
player.Stop()
|
||||
} else {
|
||||
playSCURL(p, command)
|
||||
sendUserTune(client, "Radiohead", "Spectre")
|
||||
playSCURL(player, command)
|
||||
sendUserTune(s, "Radiohead", "Spectre")
|
||||
}
|
||||
}
|
||||
|
||||
func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) {
|
||||
switch payload := packet.Payload[0].(type) {
|
||||
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 *xmpp.ControlSet:
|
||||
case *stanza.ControlSet:
|
||||
var url string
|
||||
for _, element := range payload.Fields {
|
||||
if element.XMLName.Local == "string" && element.Name == "url" {
|
||||
@@ -72,23 +90,24 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) {
|
||||
}
|
||||
}
|
||||
|
||||
playSCURL(p, url)
|
||||
setResponse := new(xmpp.ControlSetResponse)
|
||||
reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: []xmpp.IQPayload{setResponse}}
|
||||
_ = client.Send(reply)
|
||||
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(client, "Radiohead", "Spectre")
|
||||
sendUserTune(s, "Radiohead", "Spectre")
|
||||
default:
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", packet.Payload)
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", iq.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
func sendUserTune(client *xmpp.Client, artist string, title string) {
|
||||
tune := xmpp.Tune{Artist: artist, Title: title}
|
||||
iq := xmpp.NewIQ("set", "", "", "usertune-1", "en")
|
||||
payload := xmpp.PubSub{Publish: xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: tune}}}
|
||||
iq.AddPayload(&payload)
|
||||
_ = client.Send(iq)
|
||||
func 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) {
|
||||
@@ -99,20 +118,6 @@ func playSCURL(p *mpg123.Player, rawURL string) {
|
||||
_ = p.Play(url)
|
||||
}
|
||||
|
||||
func connectXmpp(jid string, password string, address string) (client *xmpp.Client, err error) {
|
||||
xmppConfig := xmpp.Config{Address: address,
|
||||
Jid: jid, Password: password, PacketLogger: os.Stdout, Insecure: true}
|
||||
|
||||
if client, err = xmpp.NewClient(xmppConfig); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = client.Connect(); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
// - Have a player API to play, play next, or add to queue
|
||||
// - Have the ability to parse custom packet to play sound
|
||||
|
||||
80
auth.go
80
auth.go
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f StreamFeatures, user string, password string) (err error) {
|
||||
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 {
|
||||
@@ -30,17 +32,17 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
|
||||
raw := "\x00" + user + "\x00" + password
|
||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
|
||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
|
||||
|
||||
// Next message should be either success or failure.
|
||||
val, err := next(decoder)
|
||||
val, err := stanza.NextPacket(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case SASLSuccess:
|
||||
case SASLFailure:
|
||||
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)
|
||||
@@ -49,69 +51,3 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SASLSuccess
|
||||
|
||||
type SASLSuccess struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
||||
}
|
||||
|
||||
func (SASLSuccess) Name() string {
|
||||
return "sasl:success"
|
||||
}
|
||||
|
||||
type saslSuccessDecoder struct{}
|
||||
|
||||
var saslSuccess saslSuccessDecoder
|
||||
|
||||
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
|
||||
var packet SASLSuccess
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SASLFailure
|
||||
|
||||
type SASLFailure struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
||||
Any xml.Name // error reason is a subelement
|
||||
}
|
||||
|
||||
func (SASLFailure) Name() string {
|
||||
return "sasl:failure"
|
||||
}
|
||||
|
||||
type saslFailureDecoder struct{}
|
||||
|
||||
var saslFailure saslFailureDecoder
|
||||
|
||||
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
|
||||
var packet SASLFailure
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
type auth struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
||||
Mechanism string `xml:"mecanism,attr"`
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type BindBind struct {
|
||||
IQPayload
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
||||
Resource string `xml:"resource,omitempty"`
|
||||
Jid string `xml:"jid,omitempty"`
|
||||
}
|
||||
|
||||
// Session is obsolete in RFC 6121.
|
||||
// Added for compliance with RFC 3121.
|
||||
// Remove when ejabberd purely conforms to RFC 6121.
|
||||
type sessionSession struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
||||
optional xml.Name // If it does exist, it mean we are not required to open session
|
||||
}
|
||||
|
||||
30
backoff.go
30
backoff.go
@@ -13,14 +13,14 @@ It can be used in several ways:
|
||||
- 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)
|
||||
- Keep the attempt counter on your end and use durationForAttempt(int)
|
||||
- Use lock in your own code to protect the Backoff structure.
|
||||
|
||||
TODO: Implement Backoff Ticker channel
|
||||
TODO: Implement throttler interface. Throttler could be used to implement various reconnect strategies.
|
||||
*/
|
||||
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"math"
|
||||
@@ -34,11 +34,11 @@ const (
|
||||
defaultCap int = 180000 // 3 minutes
|
||||
)
|
||||
|
||||
// Backoff can provide increasing duration with the number of attempt
|
||||
// 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 {
|
||||
type backoff struct {
|
||||
NoJitter bool
|
||||
Base int
|
||||
Factor int
|
||||
@@ -47,20 +47,20 @@ type Backoff struct {
|
||||
attempt int
|
||||
}
|
||||
|
||||
// Duration returns the duration to apply to the current attempt.
|
||||
func (b *Backoff) Duration() time.Duration {
|
||||
d := b.DurationForAttempt(b.attempt)
|
||||
// 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())
|
||||
// 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 {
|
||||
// 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))
|
||||
@@ -70,13 +70,13 @@ func (b *Backoff) DurationForAttempt(attempt int) time.Duration {
|
||||
return time.Duration(d) * time.Millisecond
|
||||
}
|
||||
|
||||
// Reset sets back the number of attempts to 0. This is to be called after a successfull operation has been performed,
|
||||
// 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() {
|
||||
func (b *backoff) reset() {
|
||||
b.attempt = 0
|
||||
}
|
||||
|
||||
func (b *Backoff) setDefault() {
|
||||
func (b *backoff) setDefault() {
|
||||
if b.Base == 0 {
|
||||
b.Base = defaultBase
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
package xmpp_test
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
)
|
||||
|
||||
func TestDurationForAttempt_NoJitter(t *testing.T) {
|
||||
b := xmpp.Backoff{Base: 25, NoJitter: true}
|
||||
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)
|
||||
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)
|
||||
d = b.durationForAttempt(i)
|
||||
if !(d >= prevDuration) {
|
||||
t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// TODO: Should I move this as an extension of the client?
|
||||
@@ -49,28 +51,28 @@ func (c *ServerCheck) Check() error {
|
||||
decoder := xml.NewDecoder(tcpconn)
|
||||
|
||||
// Send stream open tag
|
||||
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, NSClient, NSStream); err != nil {
|
||||
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 = initDecoder(decoder)
|
||||
_, err = stanza.InitStream(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// extract stream features
|
||||
var f StreamFeatures
|
||||
packet, err := next(decoder)
|
||||
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 StreamFeatures:
|
||||
case stanza.StreamFeatures:
|
||||
f = p
|
||||
case StreamError:
|
||||
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())
|
||||
@@ -79,13 +81,14 @@ func (c *ServerCheck) Check() error {
|
||||
if _, ok := f.DoesStartTLS(); ok {
|
||||
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||
|
||||
var k tlsProceed
|
||||
var k stanza.TLSProceed
|
||||
if err = decoder.DecodeElement(&k, nil); err != nil {
|
||||
return fmt.Errorf("expecting starttls proceed: %s", err)
|
||||
}
|
||||
|
||||
DefaultTlsConfig.ServerName = c.domain
|
||||
tlsConn := tls.Client(tcpconn, &DefaultTlsConfig)
|
||||
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
|
||||
163
client.go
163
client.go
@@ -1,12 +1,13 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
//=============================================================================
|
||||
@@ -30,6 +31,18 @@ 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
|
||||
@@ -51,6 +64,13 @@ func (em EventManager) updateState(state ConnState) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -70,8 +90,8 @@ type Client struct {
|
||||
Session *Session
|
||||
// TCP level connection / can be replaced by a TLS session after starttls
|
||||
conn net.Conn
|
||||
// Packet channel
|
||||
RecvChannel chan Packet
|
||||
// Router is used to dispatch packets
|
||||
router *Router
|
||||
// Track and broadcast connection state
|
||||
EventManager
|
||||
}
|
||||
@@ -83,10 +103,10 @@ Setting up the client / Checking the parameters
|
||||
// NewClient generates a new XMPP client, based on Config passed as parameters.
|
||||
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
|
||||
// Default the port to 5222.
|
||||
// TODO: better config checks
|
||||
func NewClient(config Config) (c *Client, err error) {
|
||||
// TODO: If option address is nil, use the Jid domain to compose the address
|
||||
if config.Address, err = checkAddress(config.Address); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -95,45 +115,47 @@ func NewClient(config Config) (c *Client, err error) {
|
||||
return nil, NewConnError(err, true)
|
||||
}
|
||||
|
||||
// Parse JID
|
||||
if config.parsedJid, err = NewJid(config.Jid); err != nil {
|
||||
err = errors.New("missing jid")
|
||||
return nil, NewConnError(err, true)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Create a default channel that developers can override
|
||||
c.RecvChannel = make(chan Packet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Pass JID to be able to add default address based on JID, if addr is empty
|
||||
func checkAddress(addr string) (string, error) {
|
||||
var err error
|
||||
hostport := strings.Split(addr, ":")
|
||||
if len(hostport) > 2 {
|
||||
err = errors.New("too many colons in xmpp server address")
|
||||
return addr, err
|
||||
}
|
||||
|
||||
// Address is composed of two parts, we are good
|
||||
if len(hostport) == 2 && hostport[1] != "" {
|
||||
return addr, err
|
||||
}
|
||||
|
||||
// Port was not passed, we append XMPP default port:
|
||||
return strings.Join([]string{hostport[0], "5222"}, ":"), err
|
||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
||||
// Connect simply triggers resumption, with an empty session state.
|
||||
func (c *Client) Connect() error {
|
||||
var state SMState
|
||||
return c.Resume(state)
|
||||
}
|
||||
|
||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
||||
func (c *Client) Connect() error {
|
||||
// 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)
|
||||
@@ -143,22 +165,23 @@ func (c *Client) Connect() error {
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
if c.conn, c.Session, err = NewSession(c.conn, c.config); err != nil {
|
||||
if c.conn, c.Session, err = NewSession(c.conn, c.config, state); err != nil {
|
||||
return err
|
||||
}
|
||||
c.updateState(StateSessionEstablished)
|
||||
|
||||
// We're connected and can now receive and send messages.
|
||||
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
||||
// TODO: Do we always want to send initial presence automatically ?
|
||||
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
||||
fmt.Fprintf(c.Session.socketProxy, "<presence/>")
|
||||
|
||||
// Start the keepalive go routine
|
||||
keepaliveQuit := make(chan struct{})
|
||||
go keepalive(c.conn, keepaliveQuit)
|
||||
// Start the receiver go routine
|
||||
go c.recv(keepaliveQuit)
|
||||
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
|
||||
}
|
||||
@@ -166,31 +189,29 @@ func (c *Client) Connect() error {
|
||||
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()
|
||||
conn := c.conn
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetHandler(handler EventHandler) {
|
||||
c.Handler = handler
|
||||
}
|
||||
|
||||
// Recv abstracts receiving preparsed XMPP packets from a channel.
|
||||
// Channel allow client to receive / dispatch packets in for range loop.
|
||||
// TODO: Deprecate this function in favor of reading directly from the RecvChannel ?
|
||||
func (c *Client) Recv() <-chan Packet {
|
||||
return c.RecvChannel
|
||||
}
|
||||
|
||||
// Send marshals XMPP stanza and sends it to the server.
|
||||
func (c *Client) Send(packet Packet) error {
|
||||
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())
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(c.conn, string(data)); err != nil {
|
||||
return errors.New("cannot send packet " + err.Error())
|
||||
}
|
||||
return nil
|
||||
return c.sendWithLogger(string(data))
|
||||
}
|
||||
|
||||
// SendRaw sends an XMPP stanza as a string to the server.
|
||||
@@ -198,8 +219,17 @@ func (c *Client) Send(packet Packet) error {
|
||||
// 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.socketProxy, packet)
|
||||
_, err = fmt.Fprintf(c.Session.streamLogger, packet)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -207,25 +237,34 @@ func (c *Client) SendRaw(packet string) error {
|
||||
// Go routines
|
||||
|
||||
// Loop: Receive data from server
|
||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
||||
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) {
|
||||
for {
|
||||
val, err := next(c.Session.decoder)
|
||||
val, err := stanza.NextPacket(c.Session.decoder)
|
||||
if err != nil {
|
||||
close(keepaliveQuit)
|
||||
c.updateState(StateDisconnected)
|
||||
c.disconnected(state)
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle stream errors
|
||||
switch packet := val.(type) {
|
||||
case StreamError:
|
||||
c.RecvChannel <- val
|
||||
close(c.RecvChannel)
|
||||
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.RecvChannel <- val
|
||||
c.router.route(c, val)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
102
client_test.go
102
client_test.go
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,7 +29,8 @@ func TestClient_Connect(t *testing.T) {
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
if client, err = NewClient(config); err != nil {
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -48,7 +51,8 @@ func TestClient_NoInsecure(t *testing.T) {
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
if client, err = NewClient(config); err != nil {
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -71,7 +75,8 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
if client, err = NewClient(config); err != nil {
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -81,7 +86,28 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
@@ -89,6 +115,7 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
||||
|
||||
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)
|
||||
@@ -109,6 +136,21 @@ func handlerAbortTLS(t *testing.T, c net.Conn) {
|
||||
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{})
|
||||
@@ -123,11 +165,11 @@ func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||
switch elem := token.(type) {
|
||||
// Wait for first startElement
|
||||
case xml.StartElement:
|
||||
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
|
||||
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", NSClient, NSStream); err != nil {
|
||||
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
|
||||
t.Errorf("cannot write server stream open: %s", err)
|
||||
}
|
||||
return
|
||||
@@ -149,14 +191,14 @@ func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
||||
|
||||
// TODO return err in case of error reading the auth params
|
||||
func readAuth(t *testing.T, decoder *xml.Decoder) string {
|
||||
se, err := nextStart(decoder)
|
||||
se, err := stanza.NextStart(decoder)
|
||||
if err != nil {
|
||||
t.Errorf("cannot read auth: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var nv interface{}
|
||||
nv = &auth{}
|
||||
nv = &stanza.SASLAuth{}
|
||||
// Decode element into pointer storage
|
||||
if err = decoder.DecodeElement(nv, &se); err != nil {
|
||||
t.Errorf("cannot decode auth: %s", err)
|
||||
@@ -164,14 +206,14 @@ func readAuth(t *testing.T, decoder *xml.Decoder) string {
|
||||
}
|
||||
|
||||
switch v := nv.(type) {
|
||||
case *auth:
|
||||
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: SASL Plain Auth
|
||||
// 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>`
|
||||
@@ -180,14 +222,25 @@ func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
||||
}
|
||||
}
|
||||
|
||||
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 := nextStart(decoder)
|
||||
se, err := stanza.NextStart(decoder)
|
||||
if err != nil {
|
||||
t.Errorf("cannot read bind: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
iq := &IQ{}
|
||||
iq := &stanza.IQ{}
|
||||
// Decode element into pointer storage
|
||||
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
||||
t.Errorf("cannot decode bind iq: %s", err)
|
||||
@@ -195,8 +248,8 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||
}
|
||||
|
||||
// TODO Check all elements
|
||||
switch iq.Payload[0].(type) {
|
||||
case *BindBind:
|
||||
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>
|
||||
@@ -205,3 +258,24 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
198
cmd/fluuxmpp/README.md
Normal file
198
cmd/fluuxmpp/README.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# fluuxmpp
|
||||
|
||||
fluuxIO's XMPP command-line tool
|
||||
|
||||
## Installation
|
||||
|
||||
To install `fluuxmpp` in your Go path:
|
||||
|
||||
```
|
||||
$ go get -u gosrc.io/xmpp/cmd/fluuxmpp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ fluuxmpp --help
|
||||
fluuxIO's xmpp comandline tool
|
||||
|
||||
Usage:
|
||||
fluuxmpp [command]
|
||||
|
||||
Available Commands:
|
||||
check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
|
||||
help Help about any command
|
||||
send is a command-line tool to send to send XMPP messages to users
|
||||
|
||||
Flags:
|
||||
-h, --help help for fluuxmpp
|
||||
|
||||
Use "fluuxmpp [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
### check tls
|
||||
|
||||
```
|
||||
$ fluuxmpp check --help
|
||||
is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
|
||||
|
||||
Usage:
|
||||
fluuxmpp check <host[:port]> [flags]
|
||||
|
||||
Examples:
|
||||
fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de
|
||||
|
||||
Flags:
|
||||
-d, --domain string domain if host handle multiple domains
|
||||
-h, --help help for check
|
||||
```
|
||||
|
||||
### sending messages
|
||||
|
||||
```
|
||||
$ fluuxmpp send --help
|
||||
is a command-line tool to send to send XMPP messages to users
|
||||
|
||||
Usage:
|
||||
fluuxmpp send <recipient,> [message] [flags]
|
||||
|
||||
Examples:
|
||||
fluuxmpp send to@chat.sum7.eu "Hello World!"
|
||||
|
||||
Flags:
|
||||
--addr string host[:port]
|
||||
--config string config file (default is ~/.config/fluuxmpp.yml)
|
||||
-h, --help help for send
|
||||
--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
|
||||
|
||||
### check tls
|
||||
|
||||
If you server is on standard port and XMPP domains matches the hostname you can simply use:
|
||||
|
||||
```
|
||||
$ fluuxmpp 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:
|
||||
|
||||
```
|
||||
$ fluuxmpp 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.
|
||||
|
||||
|
||||
### sending messages
|
||||
|
||||
Message from arguments:
|
||||
```bash
|
||||
$ fluuxmpp send 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 | fluuxmpp send 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
|
||||
$ fluuxmpp send 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 | fluuxmpp send 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 `fluuxmpp` with you favorite file extension (e.g. `toml`, `yml`).
|
||||
|
||||
e.g. ~/.config/fluuxmpp.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';
|
||||
|
||||
fluuxmpp send 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
|
||||
fluuxmpp send to@example.org "Hello World!" --jid bot@example.org --password secret --addr example.com:5222;
|
||||
```
|
||||
21
cmd/fluuxmpp/TODO.md
Normal file
21
cmd/fluuxmpp/TODO.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# TODO
|
||||
|
||||
## check
|
||||
### Features
|
||||
|
||||
- Use a config file to define the checks to perform as client on an XMPP server.
|
||||
|
||||
## send
|
||||
|
||||
### 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 ?)
|
||||
41
cmd/fluuxmpp/check.go
Normal file
41
cmd/fluuxmpp/check.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/bdlm/log"
|
||||
"github.com/spf13/cobra"
|
||||
"gosrc.io/xmpp"
|
||||
)
|
||||
|
||||
var domain = ""
|
||||
var cmdCheck = &cobra.Command{
|
||||
Use: "check <host[:port]>",
|
||||
Short: "is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires",
|
||||
Example: "fluuxmpp 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() {
|
||||
cmdRoot.AddCommand(cmdCheck)
|
||||
cmdCheck.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")
|
||||
}
|
||||
5
cmd/fluuxmpp/doc.go
Normal file
5
cmd/fluuxmpp/doc.go
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
|
||||
fluuxmpp: fluuxIO's xmpp comandline tool
|
||||
*/
|
||||
package main
|
||||
34
cmd/fluuxmpp/log.go
Normal file
34
cmd/fluuxmpp/log.go
Normal 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
|
||||
}
|
||||
19
cmd/fluuxmpp/main.go
Normal file
19
cmd/fluuxmpp/main.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/bdlm/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// cmdRoot represents the base command when called without any subcommands
|
||||
var cmdRoot = &cobra.Command{
|
||||
Use: "fluuxmpp",
|
||||
Short: "fluuxIO's xmpp comandline tool",
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.AddHook(&hook{})
|
||||
if err := cmdRoot.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
134
cmd/fluuxmpp/send.go
Normal file
134
cmd/fluuxmpp/send.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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 cmdSend = &cobra.Command{
|
||||
Use: "send <recipient,> [message]",
|
||||
Short: "is a command-line tool to send to send XMPP messages to users",
|
||||
Example: `fluuxmpp send 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() {
|
||||
cmdRoot.AddCommand(cmdSend)
|
||||
|
||||
cobra.OnInitialize(initConfigFile)
|
||||
cmdSend.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ~/.config/fluuxmpp.yml)")
|
||||
|
||||
cmdSend.Flags().StringP("jid", "", "", "using jid (required)")
|
||||
viper.BindPFlag("jid", cmdSend.Flags().Lookup("jid"))
|
||||
|
||||
cmdSend.Flags().StringP("password", "", "", "using password for your jid (required)")
|
||||
viper.BindPFlag("password", cmdSend.Flags().Lookup("password"))
|
||||
|
||||
cmdSend.Flags().StringP("addr", "", "", "host[:port]")
|
||||
viper.BindPFlag("addr", cmdSend.Flags().Lookup("addr"))
|
||||
|
||||
cmdSend.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 initConfigFile() {
|
||||
if configFile != "" {
|
||||
viper.SetConfigFile(configFile)
|
||||
}
|
||||
|
||||
viper.SetConfigName("fluuxmpp")
|
||||
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)
|
||||
}
|
||||
}
|
||||
28
cmd/fluuxmpp/xmppmuc.go
Normal file
28
cmd/fluuxmpp/xmppmuc.go
Normal 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
cmd/fluuxmpp/xmppsend.go
Normal file
36
cmd/fluuxmpp/xmppsend.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
13
cmd/go.mod
Normal file
13
cmd/go.mod
Normal 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
cmd/go.sum
Normal file
159
cmd/go.sum
Normal 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=
|
||||
@@ -1,28 +0,0 @@
|
||||
# XMPP Check
|
||||
|
||||
XMPP check is a tool to check TLS certificate on a remote server.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ go get -u gosrc.io/xmpp/cmd/xmpp-check
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
If you server is on standard port and XMPP domains matches the hostname you can simply use:
|
||||
|
||||
```
|
||||
$ xmpp-check myhost.net
|
||||
2019/05/16 16:04:36 All checks passed
|
||||
```
|
||||
|
||||
You can also pass the port and the XMPP domain if different from the server hostname:
|
||||
|
||||
```
|
||||
$ xmpp-check myhost.net:5222 xmppdomain.net
|
||||
2019/05/16 16:05:21 All checks passed
|
||||
```
|
||||
|
||||
Error code will be non-zero in case of error. You can thus use it directly with your usual
|
||||
monitoring scripts.
|
||||
@@ -1,3 +0,0 @@
|
||||
# TODO
|
||||
|
||||
- Use a config file to define the checks to perform as client on an XMPP server.
|
||||
@@ -1,42 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
|
||||
if len(args) == 0 {
|
||||
log.Fatal("usage: xmpp-check host[:port] [domain]")
|
||||
}
|
||||
|
||||
var address string
|
||||
var domain string
|
||||
if len(args) >= 1 {
|
||||
address = args[0]
|
||||
}
|
||||
|
||||
if len(args) >= 2 {
|
||||
domain = args[1]
|
||||
}
|
||||
|
||||
runCheck(address, domain)
|
||||
}
|
||||
|
||||
func runCheck(address, domain string) {
|
||||
client, err := xmpp.NewChecker(address, domain)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Error: ", err)
|
||||
}
|
||||
|
||||
if err = client.Check(); err != nil {
|
||||
log.Fatal("Failed connection check: ", err)
|
||||
}
|
||||
|
||||
log.Println("All checks passed")
|
||||
}
|
||||
110
component.go
110
component.go
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
|
||||
@@ -37,8 +39,6 @@ type ComponentOptions struct {
|
||||
// =================================
|
||||
// Communication with developer client / StreamManager
|
||||
|
||||
// Packet channel
|
||||
RecvChannel chan Packet
|
||||
// Track and broadcast connection state
|
||||
EventManager
|
||||
}
|
||||
@@ -48,6 +48,7 @@ type ComponentOptions struct {
|
||||
// in XEP-0114, XEP-0355 and XEP-0356.
|
||||
type Component struct {
|
||||
ComponentOptions
|
||||
router *Router
|
||||
|
||||
// TCP level connection
|
||||
conn net.Conn
|
||||
@@ -57,78 +58,85 @@ type Component struct {
|
||||
decoder *xml.Decoder
|
||||
}
|
||||
|
||||
func NewComponent(opts ComponentOptions) (*Component, error) {
|
||||
c := Component{ComponentOptions: opts}
|
||||
// Create a default channel that developers can override
|
||||
c.RecvChannel = make(chan Packet)
|
||||
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 state SMState
|
||||
return c.Resume(state)
|
||||
}
|
||||
func (c *Component) Resume(sm SMState) 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
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// 1. Send stream open tag
|
||||
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, NSComponent, NSStream); err != nil {
|
||||
return errors.New("cannot send stream open " + err.Error())
|
||||
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
|
||||
c.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("cannot send stream open "+err.Error()), false)
|
||||
}
|
||||
c.decoder = xml.NewDecoder(conn)
|
||||
|
||||
// 2. Initialize xml decoder and extract streamID from reply
|
||||
streamId, err := initDecoder(c.decoder)
|
||||
streamId, err := stanza.InitStream(c.decoder)
|
||||
if err != nil {
|
||||
return errors.New("cannot init decoder " + err.Error())
|
||||
c.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("cannot init decoder "+err.Error()), false)
|
||||
}
|
||||
|
||||
// 3. Authentication
|
||||
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
|
||||
return errors.New("cannot send handshake " + err.Error())
|
||||
c.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
|
||||
}
|
||||
|
||||
// 4. Check server response for authentication
|
||||
val, err := next(c.decoder)
|
||||
val, err := stanza.NextPacket(c.decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
c.updateState(StateDisconnected)
|
||||
return NewConnError(err, true)
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case StreamError:
|
||||
return errors.New("handshake failed " + v.Error.Local)
|
||||
case Handshake:
|
||||
case stanza.StreamError:
|
||||
c.streamError("conflict", "no auth loop")
|
||||
return NewConnError(errors.New("handshake failed "+v.Error.Local), true)
|
||||
case stanza.Handshake:
|
||||
// Start the receiver go routine
|
||||
c.updateState(StateSessionEstablished)
|
||||
go c.recv()
|
||||
return nil
|
||||
default:
|
||||
return errors.New("expecting handshake result, got " + v.Name())
|
||||
c.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
conn := c.conn
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Component) SetHandler(handler EventHandler) {
|
||||
c.Handler = handler
|
||||
}
|
||||
|
||||
// Recv abstracts receiving preparsed XMPP packets from a channel.
|
||||
// Channel allow client to receive / dispatch packets in for range loop.
|
||||
// TODO: Deprecate this function in favor of reading directly from the RecvChannel ?
|
||||
func (c *Component) Recv() <-chan Packet {
|
||||
return c.RecvChannel
|
||||
}
|
||||
|
||||
// Receiver Go routine receiver
|
||||
func (c *Component) recv() (err error) {
|
||||
for {
|
||||
val, err := next(c.decoder)
|
||||
val, err := stanza.NextPacket(c.decoder)
|
||||
if err != nil {
|
||||
c.updateState(StateDisconnected)
|
||||
return err
|
||||
@@ -136,24 +144,28 @@ func (c *Component) recv() (err error) {
|
||||
|
||||
// Handle stream errors
|
||||
switch p := val.(type) {
|
||||
case StreamError:
|
||||
c.RecvChannel <- val
|
||||
close(c.RecvChannel)
|
||||
case stanza.StreamError:
|
||||
c.router.route(c, val)
|
||||
c.streamError(p.Error.Local, p.Text)
|
||||
return errors.New("stream error: " + p.Error.Local)
|
||||
}
|
||||
c.RecvChannel <- val
|
||||
c.router.route(c, val)
|
||||
}
|
||||
}
|
||||
|
||||
// Send marshalls XMPP stanza and sends it to the server.
|
||||
func (c *Component) Send(packet Packet) error {
|
||||
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(c.conn, string(data)); err != nil {
|
||||
if _, err := fmt.Fprintf(conn, string(data)); err != nil {
|
||||
return errors.New("cannot send packet " + err.Error())
|
||||
}
|
||||
return nil
|
||||
@@ -164,6 +176,11 @@ func (c *Component) Send(packet Packet) error {
|
||||
// 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
|
||||
@@ -186,33 +203,6 @@ func (c *Component) handshake(streamId string) string {
|
||||
return encodedStr
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Handshake Stanza
|
||||
|
||||
// Handshake is a stanza used by XMPP components to authenticate on XMPP
|
||||
// component port.
|
||||
type Handshake struct {
|
||||
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||
// TODO Add handshake value with test for proper serialization
|
||||
// Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (Handshake) Name() string {
|
||||
return "component:handshake"
|
||||
}
|
||||
|
||||
// Handshake decoding wrapper
|
||||
|
||||
type handshakeDecoder struct{}
|
||||
|
||||
var handshake handshakeDecoder
|
||||
|
||||
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
|
||||
var packet Handshake
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Add support for discovery management directly in component
|
||||
TODO: Support multiple identities on disco info
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandshake(t *testing.T) {
|
||||
opts := ComponentOptions{
|
||||
@@ -21,3 +23,10 @@ func TestHandshake(t *testing.T) {
|
||||
func TestGenerateHandshake(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Test that NewStreamManager can accept a Component.
|
||||
//
|
||||
// This validates that Component conforms to StreamClient interface.
|
||||
func TestStreamManager(t *testing.T) {
|
||||
NewStreamManager(&Component{}, nil)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
@@ -10,9 +11,12 @@ type Config struct {
|
||||
Jid string
|
||||
parsedJid *Jid // For easier manipulation
|
||||
Password string
|
||||
PacketLogger *os.File // Used for debugging
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
18
doc.go
18
doc.go
@@ -1,13 +1,23 @@
|
||||
/*
|
||||
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
||||
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 simple adhoc XMPP clients:
|
||||
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
|
||||
|
||||
Fluux XMPP can be used to build XMPP clients or XMPP components.
|
||||
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
|
||||
|
||||
@@ -29,4 +39,4 @@ Fluux XMPP has been primarily tested with ejabberd (https://www.ejabberd.im)
|
||||
but it should work with any XMPP compliant server.
|
||||
|
||||
*/
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
4
go.mod
4
go.mod
@@ -1,8 +1,8 @@
|
||||
module gosrc.io/xmpp
|
||||
|
||||
go 1.9
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.2.0
|
||||
github.com/google/go-cmp v0.3.0
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,4 +1,4 @@
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
341
iq.go
341
iq.go
@@ -1,341 +0,0 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO support ability to put Raw payload inside IQ
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// XMPP Errors
|
||||
|
||||
// Err is an XMPP stanza payload that is used to report error on message,
|
||||
// presence or iq stanza.
|
||||
// It is intended to be added in the payload of the erroneous stanza.
|
||||
type Err struct {
|
||||
IQPayload
|
||||
XMLName xml.Name `xml:"error"`
|
||||
Code int `xml:"code,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Reason string
|
||||
Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for IQs
|
||||
func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
x.XMLName = start.Name
|
||||
|
||||
// Extract attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "type" {
|
||||
x.Type = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "code" {
|
||||
if code, err := strconv.Atoi(attr.Value); err == nil {
|
||||
x.Code = code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check subelements to extract error text and reason (from local namespace).
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
elt := new(Node)
|
||||
|
||||
err = d.DecodeElement(elt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||
if elt.XMLName == textName {
|
||||
x.Text = string(elt.Content)
|
||||
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
|
||||
x.Reason = elt.XMLName.Local
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
if x.Code == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode start element and attributes
|
||||
start.Name = xml.Name{Local: "error"}
|
||||
|
||||
code := xml.Attr{
|
||||
Name: xml.Name{Local: "code"},
|
||||
Value: strconv.Itoa(x.Code),
|
||||
}
|
||||
start.Attr = append(start.Attr, code)
|
||||
|
||||
if len(x.Type) > 0 {
|
||||
typ := xml.Attr{
|
||||
Name: xml.Name{Local: "type"},
|
||||
Value: x.Type,
|
||||
}
|
||||
start.Attr = append(start.Attr, typ)
|
||||
}
|
||||
err = e.EncodeToken(start)
|
||||
|
||||
// SubTags
|
||||
// Reason
|
||||
if x.Reason != "" {
|
||||
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
|
||||
e.EncodeToken(xml.StartElement{Name: reason})
|
||||
e.EncodeToken(xml.EndElement{Name: reason})
|
||||
}
|
||||
|
||||
// Text
|
||||
if x.Text != "" {
|
||||
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||
e.EncodeToken(xml.StartElement{Name: text})
|
||||
e.EncodeToken(xml.CharData(x.Text))
|
||||
e.EncodeToken(xml.EndElement{Name: text})
|
||||
}
|
||||
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IQ Packet
|
||||
|
||||
type IQ struct { // Info/Query
|
||||
XMLName xml.Name `xml:"iq"`
|
||||
PacketAttrs
|
||||
Payload []IQPayload `xml:",omitempty"`
|
||||
RawXML string `xml:",innerxml"`
|
||||
Error Err `xml:"error,omitempty"`
|
||||
}
|
||||
|
||||
func NewIQ(iqtype, from, to, id, lang string) IQ {
|
||||
return IQ{
|
||||
XMLName: xml.Name{Local: "iq"},
|
||||
PacketAttrs: PacketAttrs{
|
||||
Id: id,
|
||||
From: from,
|
||||
To: to,
|
||||
Type: iqtype,
|
||||
Lang: lang,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (iq *IQ) AddPayload(payload IQPayload) {
|
||||
iq.Payload = append(iq.Payload, payload)
|
||||
}
|
||||
|
||||
func (iq IQ) MakeError(xerror Err) IQ {
|
||||
from := iq.From
|
||||
to := iq.To
|
||||
|
||||
iq.Type = "error"
|
||||
iq.From = to
|
||||
iq.To = from
|
||||
iq.Error = xerror
|
||||
|
||||
return iq
|
||||
}
|
||||
|
||||
func (IQ) Name() string {
|
||||
return "iq"
|
||||
}
|
||||
|
||||
type iqDecoder struct{}
|
||||
|
||||
var iq iqDecoder
|
||||
|
||||
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
|
||||
var packet IQ
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for IQs
|
||||
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
iq.XMLName = start.Name
|
||||
|
||||
// Extract IQ attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "id" {
|
||||
iq.Id = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "type" {
|
||||
iq.Type = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "to" {
|
||||
iq.To = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "from" {
|
||||
iq.From = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "lang" {
|
||||
iq.Lang = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// decode inner elements
|
||||
level := 0
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
level++
|
||||
if level <= 1 {
|
||||
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
||||
// Decode payload extension
|
||||
err = d.DecodeElement(iqExt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Payload = append(iq.Payload, iqExt)
|
||||
} else {
|
||||
// TODO: Fix me. We do nothing of that element here.
|
||||
// elt = new(Node)
|
||||
}
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
level--
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Generic IQ Payload
|
||||
|
||||
type IQPayload interface{}
|
||||
|
||||
// Node is a generic structure to represent XML data. It is used to parse
|
||||
// unreferenced or custom stanza payload.
|
||||
type Node struct {
|
||||
IQPayload
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content string `xml:",innerxml"`
|
||||
Nodes []Node `xml:",any"`
|
||||
}
|
||||
|
||||
// Attr represents generic XML attributes, as used on the generic XML Node
|
||||
// representation.
|
||||
type Attr struct {
|
||||
K string
|
||||
V string
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
|
||||
for _, attr := range start.Attr {
|
||||
// Do not repeat xmlns, it is already in XMLName
|
||||
if attr.Name.Local != "xmlns" {
|
||||
n.Attrs = append(n.Attrs, attr)
|
||||
}
|
||||
}
|
||||
type node Node
|
||||
return d.DecodeElement((*node)(n), &start)
|
||||
}
|
||||
|
||||
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
|
||||
// Node structure to XML.
|
||||
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
start.Attr = n.Attrs
|
||||
start.Name = n.XMLName
|
||||
|
||||
err = e.EncodeToken(start)
|
||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Disco
|
||||
|
||||
const (
|
||||
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
||||
NSDiscoItems = "http://jabber.org/protocol/disco#items"
|
||||
)
|
||||
|
||||
// Disco Info
|
||||
type DiscoInfo struct {
|
||||
IQPayload
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Identity Identity `xml:"identity"`
|
||||
Features []Feature `xml:"feature"`
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
XMLName xml.Name `xml:"identity,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Category string `xml:"category,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
XMLName xml.Name `xml:"feature"`
|
||||
Var string `xml:"var,attr"`
|
||||
}
|
||||
|
||||
// Disco Items
|
||||
type DiscoItems struct {
|
||||
IQPayload
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Items []DiscoItem `xml:"item"`
|
||||
}
|
||||
|
||||
type DiscoItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
JID string `xml:"jid,attr,omitempty"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Software Version (XEP-0092)
|
||||
|
||||
// Version
|
||||
type Version struct {
|
||||
IQPayload
|
||||
XMLName xml.Name `xml:"jabber:iq:version query"`
|
||||
Name string `xml:"name,omitempty"`
|
||||
Version string `xml:"version,omitempty"`
|
||||
OS string `xml:"os,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
|
||||
}
|
||||
2
jid.go
2
jid.go
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
32
network.go
Normal file
32
network.go
Normal 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
network_test.go
Normal file
35
network_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
ns.go
11
ns.go
@@ -1,11 +0,0 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
|
||||
const (
|
||||
NSStream = "http://etherx.jabber.org/streams"
|
||||
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
||||
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||
NSClient = "jabber:client"
|
||||
NSComponent = "jabber:component:accept"
|
||||
)
|
||||
18
packet.go
18
packet.go
@@ -1,18 +0,0 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
|
||||
type Packet interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// PacketAttrs represents the common structure for base XMPP packets.
|
||||
type PacketAttrs struct {
|
||||
Id string `xml:"id,attr,omitempty"`
|
||||
From string `xml:"from,attr,omitempty"`
|
||||
To string `xml:"to,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Lang string `xml:"lang,attr,omitempty"`
|
||||
}
|
||||
|
||||
type packetFormatter interface {
|
||||
XMPPFormat() string
|
||||
}
|
||||
83
pep.go
83
pep.go
@@ -1,83 +0,0 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type PubSub struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
|
||||
Publish Publish
|
||||
}
|
||||
|
||||
type Publish struct {
|
||||
XMLName xml.Name `xml:"publish"`
|
||||
Node string `xml:"node,attr"`
|
||||
Item Item
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Tune Tune
|
||||
}
|
||||
|
||||
type Tune struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/tune tune"`
|
||||
Artist string `xml:"artist,omitempty"`
|
||||
Length int `xml:"length,omitempty"`
|
||||
Rating int `xml:"rating,omitempty"`
|
||||
Source string `xml:"source,omitempty"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
Track string `xml:"track,omitempty"`
|
||||
Uri string `xml:"uri,omitempty"`
|
||||
}
|
||||
|
||||
/*
|
||||
type PubsubPublish struct {
|
||||
XMLName xml.Name `xml:"publish"`
|
||||
node string `xml:"node,attr"`
|
||||
item PubSubItem
|
||||
}
|
||||
|
||||
type PubSubItem struct {
|
||||
xmlName xml.Name `xml:"item"`
|
||||
}
|
||||
|
||||
type Thing2 struct {
|
||||
XMLName xml.Name `xml:"publish"`
|
||||
node string `xml:"node,attr"`
|
||||
tune string `xml:"http://jabber.org/protocol/tune item>tune"`
|
||||
}
|
||||
|
||||
type Tune struct {
|
||||
artist string
|
||||
length int
|
||||
rating int
|
||||
source string
|
||||
title string
|
||||
track string
|
||||
uri string
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func (*Tune) XMPPFormat() string {
|
||||
return fmt.Sprintf(
|
||||
`<iq type='set' id='%s'>
|
||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||
<publish node='http://jabber.org/protocol/tune'>
|
||||
<item>
|
||||
<tune xmlns='http://jabber.org/protocol/tune'>
|
||||
<artist>%s</artist>
|
||||
<length>%i</length>
|
||||
<rating>%i</rating>
|
||||
<source>%s</source>
|
||||
<title>%s</title>
|
||||
<track>%s</track>
|
||||
<uri>%s</uri>
|
||||
</tune>
|
||||
</item>
|
||||
</publish>
|
||||
</pubsub>
|
||||
</iq>`)
|
||||
}
|
||||
*/
|
||||
42
presence.go
42
presence.go
@@ -1,42 +0,0 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
// Presence Packet
|
||||
|
||||
type Presence struct {
|
||||
XMLName xml.Name `xml:"presence"`
|
||||
PacketAttrs
|
||||
Show string `xml:"show,omitempty"` // away, chat, dnd, xa
|
||||
Status string `xml:"status,omitempty"`
|
||||
Priority string `xml:"priority,omitempty"`
|
||||
Error Err `xml:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (Presence) Name() string {
|
||||
return "presence"
|
||||
}
|
||||
|
||||
func NewPresence(from, to, id, lang string) Presence {
|
||||
return Presence{
|
||||
XMLName: xml.Name{Local: "presence"},
|
||||
PacketAttrs: PacketAttrs{
|
||||
Id: id,
|
||||
From: from,
|
||||
To: to,
|
||||
Lang: lang,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type presenceDecoder struct{}
|
||||
|
||||
var presence presenceDecoder
|
||||
|
||||
func (presenceDecoder) decode(p *xml.Decoder, se xml.StartElement) (Presence, error) {
|
||||
var packet Presence
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
// TODO Add default presence type (when omitted)
|
||||
return packet, err
|
||||
}
|
||||
259
router.go
Normal file
259
router.go
Normal file
@@ -0,0 +1,259 @@
|
||||
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.
|
||||
// You can register attach them to a route via the AddMatcher method.
|
||||
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
router_test.go
Normal file
252
router_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
174
session.go
174
session.go
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"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'>"
|
||||
@@ -15,45 +17,57 @@ type Session struct {
|
||||
// Session info
|
||||
BindJid string // Jabber ID as provided by XMPP server
|
||||
StreamId string
|
||||
Features StreamFeatures
|
||||
SMState SMState
|
||||
Features stanza.StreamFeatures
|
||||
TlsEnabled bool
|
||||
lastPacketId int
|
||||
|
||||
// Session interface
|
||||
In chan interface{}
|
||||
Out chan interface{}
|
||||
|
||||
// read / write
|
||||
socketProxy io.ReadWriter
|
||||
decoder *xml.Decoder
|
||||
streamLogger io.ReadWriter
|
||||
decoder *xml.Decoder
|
||||
|
||||
// error management
|
||||
err error
|
||||
}
|
||||
|
||||
func NewSession(conn net.Conn, o Config) (net.Conn, *Session, 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)
|
||||
if s.TlsEnabled {
|
||||
s.reset(conn, tlsConn, o)
|
||||
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o)
|
||||
|
||||
if s.err != nil {
|
||||
return nil, nil, NewConnError(s.err, true)
|
||||
}
|
||||
|
||||
if !s.TlsEnabled && !o.Insecure {
|
||||
return nil, nil, NewConnError(errors.New("failed to negotiate TLS session"), true)
|
||||
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
||||
return nil, nil, NewConnError(err, true)
|
||||
}
|
||||
|
||||
if s.TlsEnabled {
|
||||
s.reset(conn, tlsConn, o)
|
||||
}
|
||||
|
||||
// auth
|
||||
s.auth(o)
|
||||
s.reset(tlsConn, tlsConn, o)
|
||||
|
||||
// bind resource and 'start' XMPP session
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -63,7 +77,7 @@ func (s *Session) PacketId() string {
|
||||
}
|
||||
|
||||
func (s *Session) init(conn net.Conn, o Config) {
|
||||
s.setProxy(nil, conn, o)
|
||||
s.setStreamLogger(nil, conn, o)
|
||||
s.Features = s.open(o.parsedJid.Domain)
|
||||
}
|
||||
|
||||
@@ -72,27 +86,26 @@ func (s *Session) reset(conn net.Conn, newConn net.Conn, o Config) {
|
||||
return
|
||||
}
|
||||
|
||||
s.setProxy(conn, newConn, o)
|
||||
s.setStreamLogger(conn, newConn, o)
|
||||
s.Features = s.open(o.parsedJid.Domain)
|
||||
}
|
||||
|
||||
// TODO: setProxyLogger ? better name ? This is not a TCP / HTTP proxy
|
||||
func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Config) {
|
||||
func (s *Session) setStreamLogger(conn net.Conn, newConn net.Conn, o Config) {
|
||||
if newConn != conn {
|
||||
s.socketProxy = newSocketProxy(newConn, o.PacketLogger)
|
||||
s.streamLogger = newStreamLogger(newConn, o.StreamLogger)
|
||||
}
|
||||
s.decoder = xml.NewDecoder(s.socketProxy)
|
||||
s.decoder = xml.NewDecoder(s.streamLogger)
|
||||
s.decoder.CharsetReader = o.CharsetReader
|
||||
}
|
||||
|
||||
func (s *Session) open(domain string) (f StreamFeatures) {
|
||||
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
||||
// Send stream open tag
|
||||
if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, NSClient, NSStream); s.err != nil {
|
||||
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 = initDecoder(s.decoder) // TODO refactor / rename
|
||||
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
|
||||
if s.err != nil {
|
||||
return
|
||||
}
|
||||
@@ -104,34 +117,48 @@ func (s *Session) open(domain string) (f StreamFeatures) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
||||
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.socketProxy, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||
fmt.Fprintf(s.streamLogger, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||
|
||||
var k tlsProceed
|
||||
var k stanza.TLSProceed
|
||||
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
|
||||
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
||||
return conn
|
||||
}
|
||||
s.TlsEnabled = true
|
||||
|
||||
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
|
||||
DefaultTlsConfig.ServerName = domain
|
||||
tlsConn := tls.Client(conn, &DefaultTlsConfig)
|
||||
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
|
||||
}
|
||||
|
||||
// We check that cert matches hostname
|
||||
s.err = tlsConn.VerifyHostname(domain)
|
||||
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
|
||||
}
|
||||
@@ -141,7 +168,39 @@ func (s *Session) auth(o Config) {
|
||||
return
|
||||
}
|
||||
|
||||
s.err = authSASL(s.socketProxy, s.decoder, s.Features, o.parsedJid.Node, o.Password)
|
||||
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) {
|
||||
@@ -152,21 +211,21 @@ func (s *Session) bind(o Config) {
|
||||
// Send IQ message asking to bind to the local user name.
|
||||
var resource = o.parsedJid.Resource
|
||||
if resource != "" {
|
||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
||||
s.PacketId(), nsBind, resource)
|
||||
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.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), nsBind)
|
||||
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
||||
}
|
||||
|
||||
var iq IQ
|
||||
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[0].(type) {
|
||||
case *BindBind:
|
||||
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")
|
||||
@@ -175,19 +234,46 @@ func (s *Session) bind(o Config) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: remove when ejabberd is fixed: https://github.com/processone/ejabberd/issues/869
|
||||
// After the bind, if the session is required (as per old RFC 3921), we send the session open iq
|
||||
// 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 IQ
|
||||
if s.Features.Session.optional.Local != "" {
|
||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), nsSession)
|
||||
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
stanza/README.md
Normal file
142
stanza/README.md
Normal 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
stanza/component.go
Normal file
91
stanza/component.go
Normal 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
stanza/component_test.go
Normal file
79
stanza/component_test.go
Normal 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
stanza/doc.go
Normal file
4
stanza/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
XMPP stanza package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
||||
*/
|
||||
package stanza
|
||||
110
stanza/error.go
Normal file
110
stanza/error.go
Normal 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
stanza/error_enum.go
Normal file
13
stanza/error_enum.go
Normal 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"
|
||||
)
|
||||
@@ -1,15 +1,18 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type ControlSet struct {
|
||||
IQPayload
|
||||
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"`
|
||||
}
|
||||
@@ -21,6 +24,16 @@ type ControlField struct {
|
||||
}
|
||||
|
||||
type ControlSetResponse struct {
|
||||
IQPayload
|
||||
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
|
||||
}
|
||||
|
||||
func (c *ControlSetResponse) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -20,7 +20,7 @@ func TestControlSet(t *testing.T) {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if cs, ok := parsedIQ.Payload[0].(*ControlSet); !ok {
|
||||
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
|
||||
t.Errorf("Paylod is not an iot control set: %v", cs)
|
||||
}
|
||||
}
|
||||
128
stanza/iq.go
Normal file
128
stanza/iq.go
Normal 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
stanza/iq_disco.go
Normal file
149
stanza/iq_disco.go
Normal 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
stanza/iq_disco_test.go
Normal file
90
stanza/iq_disco_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -6,22 +6,22 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestUnmarshalIqs(t *testing.T) {
|
||||
//var cs1 = new(iot.ControlSet)
|
||||
var tests = []struct {
|
||||
iqString string
|
||||
parsedIQ xmpp.IQ
|
||||
parsedIQ stanza.IQ
|
||||
}{
|
||||
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
|
||||
xmpp.IQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: xmpp.PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}},
|
||||
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 := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", test.iqString)
|
||||
@@ -35,19 +35,19 @@ func TestUnmarshalIqs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateIq(t *testing.T) {
|
||||
iq := xmpp.NewIQ("result", "admin@localhost", "test@localhost", "1", "en")
|
||||
payload := xmpp.DiscoInfo{
|
||||
Identity: xmpp.Identity{
|
||||
Name: "Test Gateway",
|
||||
Category: "gateway",
|
||||
Type: "mqtt",
|
||||
},
|
||||
Features: []xmpp.Feature{
|
||||
{Var: xmpp.NSDiscoInfo},
|
||||
{Var: xmpp.NSDiscoItems},
|
||||
iq := 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.AddPayload(&payload)
|
||||
iq.Payload = &payload
|
||||
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
@@ -58,18 +58,18 @@ func TestGenerateIq(t *testing.T) {
|
||||
t.Error("empty error should not be serialized")
|
||||
}
|
||||
|
||||
parsedIQ := xmpp.IQ{}
|
||||
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))
|
||||
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
|
||||
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorTag(t *testing.T) {
|
||||
xError := xmpp.Err{
|
||||
xError := stanza.Err{
|
||||
XMLName: xml.Name{Local: "error"},
|
||||
Code: 503,
|
||||
Type: "cancel",
|
||||
@@ -82,7 +82,7 @@ func TestErrorTag(t *testing.T) {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
}
|
||||
|
||||
parsedError := xmpp.Err{}
|
||||
parsedError := stanza.Err{}
|
||||
if err = xml.Unmarshal(data, &parsedError); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@@ -93,18 +93,18 @@ func TestErrorTag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDiscoItems(t *testing.T) {
|
||||
iq := xmpp.NewIQ("get", "romeo@montague.net/orchard", "catalog.shakespeare.lit", "items3", "en")
|
||||
payload := xmpp.DiscoItems{
|
||||
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.AddPayload(&payload)
|
||||
iq.Payload = &payload
|
||||
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@@ -117,13 +117,55 @@ func TestDiscoItems(t *testing.T) {
|
||||
func TestUnmarshalPayload(t *testing.T) {
|
||||
query := "<iq to='service.localhost' type='get' id='1'><query xmlns='jabber:iq:version'/></iq>"
|
||||
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(query), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", query)
|
||||
}
|
||||
|
||||
if len(parsedIQ.Payload) != 1 {
|
||||
t.Errorf("Incorrect payload size: %d", len(parsedIQ.Payload))
|
||||
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
stanza/iq_version.go
Normal file
45
stanza/iq_version.go
Normal 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
stanza/iq_version_test.go
Normal file
40
stanza/iq_version_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
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"`
|
||||
PacketAttrs
|
||||
Attrs
|
||||
|
||||
Subject string `xml:"subject,omitempty"`
|
||||
Body string `xml:"body,omitempty"`
|
||||
Thread string `xml:"thread,omitempty"`
|
||||
@@ -21,19 +24,44 @@ func (Message) Name() string {
|
||||
return "message"
|
||||
}
|
||||
|
||||
func NewMessage(msgtype, from, to, id, lang string) Message {
|
||||
func NewMessage(a Attrs) Message {
|
||||
return Message{
|
||||
XMLName: xml.Name{Local: "message"},
|
||||
PacketAttrs: PacketAttrs{
|
||||
Id: id,
|
||||
From: from,
|
||||
To: to,
|
||||
Type: msgtype,
|
||||
Lang: lang,
|
||||
},
|
||||
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
|
||||
@@ -53,7 +81,7 @@ func (msg *Message) XMPPFormat() string {
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for IQs
|
||||
// UnmarshalXML implements custom parsing for messages
|
||||
func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
msg.XMLName = start.Name
|
||||
|
||||
@@ -63,7 +91,7 @@ func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
msg.Id = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "type" {
|
||||
msg.Type = attr.Value
|
||||
msg.Type = StanzaType(attr.Value)
|
||||
}
|
||||
if attr.Name.Local == "to" {
|
||||
msg.To = attr.Value
|
||||
@@ -1,15 +1,15 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestGenerateMessage(t *testing.T) {
|
||||
message := xmpp.NewMessage("chat", "admin@localhost", "test@localhost", "1", "en")
|
||||
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
message.Body = "Hi"
|
||||
message.Subject = "Msg Subject"
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestGenerateMessage(t *testing.T) {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
parsedMessage := xmpp.Message{}
|
||||
parsedMessage := stanza.Message{}
|
||||
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func TestDecodeError(t *testing.T) {
|
||||
</error>
|
||||
</message>`
|
||||
|
||||
parsedMessage := xmpp.Message{}
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message error stanza unmarshall error: %v", err)
|
||||
return
|
||||
@@ -47,3 +47,30 @@ func TestDecodeError(t *testing.T) {
|
||||
t.Errorf("incorrect error type: %s", parsedMessage.Error.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOOB(t *testing.T) {
|
||||
image := "https://localhost/image.png"
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||
ext := stanza.OOB{
|
||||
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
|
||||
URL: image,
|
||||
}
|
||||
msg.Extensions = append(msg.Extensions, &ext)
|
||||
|
||||
// OOB can properly be found
|
||||
var oob stanza.OOB
|
||||
// Try to find and
|
||||
if ok := msg.Get(&oob); !ok {
|
||||
t.Error("could not find oob extension")
|
||||
return
|
||||
}
|
||||
if oob.URL != image {
|
||||
t.Errorf("OOB URL was not properly extracted: ''%s", oob.URL)
|
||||
}
|
||||
|
||||
// Markable is not found
|
||||
var m stanza.Markable
|
||||
if ok := msg.Get(&m); ok {
|
||||
t.Error("we should not have found markable extension")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
@@ -1,6 +1,8 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
22
stanza/msg_html.go
Normal file
22
stanza/msg_html.go
Normal 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
stanza/msg_html_test.go
Normal file
44
stanza/msg_html_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
@@ -1,6 +1,8 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
@@ -1,10 +1,10 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestDecodeRequest(t *testing.T) {
|
||||
@@ -15,7 +15,7 @@ func TestDecodeRequest(t *testing.T) {
|
||||
<body>My lord, dispatch; read o'er these articles.</body>
|
||||
<request xmlns='urn:xmpp:receipts'/>
|
||||
</message>`
|
||||
parsedMessage := xmpp.Message{}
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message receipt unmarshall error: %v", err)
|
||||
return
|
||||
@@ -31,7 +31,7 @@ func TestDecodeRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
switch ext := parsedMessage.Extensions[0].(type) {
|
||||
case *xmpp.ReceiptRequest:
|
||||
case *stanza.ReceiptRequest:
|
||||
if ext.XMLName.Local != "request" {
|
||||
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
51
stanza/node.go
Normal file
51
stanza/node.go
Normal 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
stanza/ns.go
Normal file
11
stanza/ns.go
Normal 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
stanza/packet.go
Normal file
18
stanza/packet.go
Normal 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
stanza/packet_enum.go
Normal file
25
stanza/packet_enum.go
Normal 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"
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -14,56 +14,41 @@ import (
|
||||
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
|
||||
// getting through the authentication process.
|
||||
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
|
||||
func initDecoder(p *xml.Decoder) (sessionID string, err error) {
|
||||
func InitStream(p *xml.Decoder) (sessionID string, err error) {
|
||||
for {
|
||||
var t xml.Token
|
||||
t, err = p.Token()
|
||||
if err != nil {
|
||||
return
|
||||
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
|
||||
return sessionID, err
|
||||
}
|
||||
|
||||
// Parse Stream attributes
|
||||
// Parse XMPP stream attributes
|
||||
for _, attrs := range elem.Attr {
|
||||
switch attrs.Name.Local {
|
||||
case "id":
|
||||
sessionID = attrs.Value
|
||||
}
|
||||
}
|
||||
return
|
||||
return sessionID, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan XML token stream to find next StartElement.
|
||||
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||
for {
|
||||
t, err := p.Token()
|
||||
if err == io.EOF {
|
||||
return xml.StartElement{}, errors.New("connection closed")
|
||||
}
|
||||
if err != nil {
|
||||
return xml.StartElement{}, fmt.Errorf("nextStart %s", err)
|
||||
}
|
||||
switch t := t.(type) {
|
||||
case xml.StartElement:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// next scans XML token stream for next element and then assign a structure to decode
|
||||
// that elements.
|
||||
// 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
|
||||
func next(p *xml.Decoder) (Packet, error) {
|
||||
// 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)
|
||||
se, err := NextStart(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,18 +57,44 @@ func next(p *xml.Decoder) (Packet, error) {
|
||||
switch se.Name.Space {
|
||||
case NSStream:
|
||||
return decodeStream(p, se)
|
||||
case nsSASL:
|
||||
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":
|
||||
@@ -96,6 +107,7 @@ func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// decodeSASL decodes a packet related to SASL authentication.
|
||||
func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
switch se.Name.Local {
|
||||
case "success":
|
||||
@@ -108,6 +120,7 @@ func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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":
|
||||
@@ -122,9 +135,10 @@ func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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":
|
||||
case "handshake": // handshake is used to authenticate components
|
||||
return handshake.decode(p, se)
|
||||
case "message":
|
||||
return message.decode(p, se)
|
||||
27
stanza/pep.go
Normal file
27
stanza/pep.go
Normal 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
stanza/pres_muc.go
Normal file
148
stanza/pres_muc.go
Normal 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
stanza/pres_muc_test.go
Normal file
97
stanza/pres_muc_test.go
Normal 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
stanza/presence.go
Normal file
139
stanza/presence.go
Normal 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
stanza/presence_enum.go
Normal file
12
stanza/presence_enum.go
Normal 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"
|
||||
)
|
||||
@@ -1,24 +1,23 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestGeneratePresence(t *testing.T) {
|
||||
presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en")
|
||||
presence.Show = "chat"
|
||||
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 xmpp.Presence
|
||||
var parsedPresence stanza.Presence
|
||||
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@@ -32,15 +31,15 @@ func TestPresenceSubElt(t *testing.T) {
|
||||
// Test structure to ensure that show, status and priority are correctly defined as presence
|
||||
// package sub-elements
|
||||
type pres struct {
|
||||
Show string `xml:"show"`
|
||||
Status string `xml:"status"`
|
||||
Priority string `xml:"priority"`
|
||||
Show stanza.PresenceShow `xml:"show"`
|
||||
Status string `xml:"status"`
|
||||
Priority int8 `xml:"priority"`
|
||||
}
|
||||
|
||||
presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en")
|
||||
presence.Show = "xa"
|
||||
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
presence.Show = stanza.PresenceShowXA
|
||||
presence.Status = "Coding"
|
||||
presence.Priority = "10"
|
||||
presence.Priority = 10
|
||||
|
||||
data, err := xml.Marshal(presence)
|
||||
if err != nil {
|
||||
@@ -59,6 +58,6 @@ func TestPresenceSubElt(t *testing.T) {
|
||||
t.Errorf("cannot read 'status' as presence subelement (%s)", parsedPresence.Status)
|
||||
}
|
||||
if parsedPresence.Priority != presence.Priority {
|
||||
t.Errorf("cannot read 'priority' as presence subelement (%s)", parsedPresence.Priority)
|
||||
t.Errorf("cannot read 'priority' as presence subelement (%d)", parsedPresence.Priority)
|
||||
}
|
||||
}
|
||||
40
stanza/pubsub.go
Normal file
40
stanza/pubsub.go
Normal 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{})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
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.
|
||||
@@ -78,6 +79,19 @@ func (r *registry) GetExtensionType(pktType PacketType, name xml.Name) reflect.T
|
||||
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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
112
stanza/sasl_auth.go
Normal file
112
stanza/sasl_auth.go
Normal 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
stanza/sasl_auth_test.go
Normal file
57
stanza/sasl_auth_test.go
Normal 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
|
||||
@@ -1,14 +1,11 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
var DefaultTlsConfig tls.Config
|
||||
|
||||
// Used during stream initiation / session establishment
|
||||
type tlsProceed struct {
|
||||
type TLSProceed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
|
||||
// ============================================================================
|
||||
// StreamFeatures Packet
|
||||
// Reference: https://xmpp.org/registrar/stream-features.html
|
||||
// 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"`
|
||||
@@ -15,9 +17,10 @@ type StreamFeatures struct {
|
||||
// Stream features
|
||||
StartTLS tlsStartTLS
|
||||
Mechanisms saslMechanisms
|
||||
Bind BindBind
|
||||
Session sessionSession
|
||||
Bind Bind
|
||||
StreamManagement streamManagement
|
||||
// Obsolete
|
||||
Session StreamSession
|
||||
// ProcessOne Stream Features
|
||||
P1Push p1Push
|
||||
P1Rebind p1Rebind
|
||||
121
stanza/stream_management.go
Normal file
121
stanza/stream_management.go
Normal 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
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestNoStartTLS(t *testing.T) {
|
||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF xmpp.StreamFeatures
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestStartTLS(t *testing.T) {
|
||||
</starttls>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF xmpp.StreamFeatures
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func TestStreamManagement(t *testing.T) {
|
||||
<sm xmlns='urn:xmpp:sm:3'/>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF xmpp.StreamFeatures
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
65
stanza/xmpp_test.go
Normal file
65
stanza/xmpp_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Marshaller / unmarshaller test
|
||||
|
||||
func checkMarshalling(t *testing.T, iq stanza.IQ) (*stanza.IQ, error) {
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshall
|
||||
var parsedIQ stanza.IQ
|
||||
err = xml.Unmarshal(data, &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
|
||||
}
|
||||
return &parsedIQ, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// XML structs comparison
|
||||
|
||||
// Compare iq structure but ignore empty namespace as they are set properly on
|
||||
// marshal / unmarshal. There is no need to manage them on the manually
|
||||
// crafted structure.
|
||||
func xmlEqual(x, y interface{}) bool {
|
||||
return cmp.Equal(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
// xmlDiff compares xml structures ignoring namespace preferences
|
||||
func xmlDiff(x, y interface{}) string {
|
||||
return cmp.Diff(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
func xmlOpts() cmp.Options {
|
||||
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
|
||||
opts := cmp.Options{
|
||||
cmp.FilterValues(func(x, y interface{}) bool {
|
||||
xx, xok := x.(xml.Name)
|
||||
yy, yok := y.(xml.Name)
|
||||
if xok && yok {
|
||||
zero := xml.Name{}
|
||||
if xx == zero || yy == zero {
|
||||
return true
|
||||
}
|
||||
if xx.Space == "" || yy.Space == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, alwaysEqual),
|
||||
}
|
||||
return opts
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -7,20 +7,20 @@ import (
|
||||
|
||||
// Mediated Read / Write on socket
|
||||
// Used if logFile from Config is not nil
|
||||
type socketProxy struct {
|
||||
type streamLogger struct {
|
||||
socket io.ReadWriter // Actual connection
|
||||
logFile *os.File
|
||||
}
|
||||
|
||||
func newSocketProxy(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
|
||||
func newStreamLogger(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
|
||||
if logFile == nil {
|
||||
return conn
|
||||
} else {
|
||||
return &socketProxy{conn, logFile}
|
||||
return &streamLogger{conn, logFile}
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *socketProxy) Read(p []byte) (n int, err error) {
|
||||
func (sp *streamLogger) Read(p []byte) (n int, err error) {
|
||||
n, err = sp.socket.Read(p)
|
||||
if n > 0 {
|
||||
sp.logFile.Write([]byte("RECV:\n")) // Prefix
|
||||
@@ -32,7 +32,7 @@ func (sp *socketProxy) Read(p []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (sp *socketProxy) Write(p []byte) (n int, err error) {
|
||||
func (sp *streamLogger) Write(p []byte) (n int, err error) {
|
||||
sp.logFile.Write([]byte("SEND:\n")) // Prefix
|
||||
for _, w := range []io.Writer{sp.socket, sp.logFile} {
|
||||
n, err = w.Write(p)
|
||||
@@ -47,3 +47,7 @@ func (sp *socketProxy) Write(p []byte) (n int, err error) {
|
||||
sp.logFile.Write([]byte("\n\n")) // Separator
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Make RECV, SEND prefixes +
|
||||
*/
|
||||
@@ -1,10 +1,12 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// The Fluux XMPP lib can manage client or component XMPP streams.
|
||||
@@ -18,12 +20,24 @@ import (
|
||||
// permanent errors to avoid useless reconnection loops.
|
||||
// - Metrics processing
|
||||
|
||||
// StreamClient is an interface used by StreamManager to control Client lifecycle,
|
||||
// set callback and trigger reconnection.
|
||||
type StreamClient interface {
|
||||
Connect() error
|
||||
Resume(state SMState) error
|
||||
Send(packet stanza.Packet) error
|
||||
SendRaw(packet string) error
|
||||
Disconnect()
|
||||
SetHandler(handler EventHandler)
|
||||
}
|
||||
|
||||
// Sender is an interface provided by Stream clients to allow sending XMPP data.
|
||||
// It is mostly use in callback to pass a limited subset of the stream client interface
|
||||
type Sender interface {
|
||||
Send(packet stanza.Packet) error
|
||||
SendRaw(packet string) error
|
||||
}
|
||||
|
||||
// StreamManager supervises an XMPP client connection. Its role is to handle connection events and
|
||||
// apply reconnection strategy.
|
||||
type StreamManager struct {
|
||||
@@ -32,9 +46,11 @@ type StreamManager struct {
|
||||
|
||||
// Store low level metrics
|
||||
Metrics *Metrics
|
||||
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type PostConnect func(c StreamClient)
|
||||
type PostConnect func(c Sender)
|
||||
|
||||
// NewStreamManager creates a new StreamManager structure, intended to support
|
||||
// handling XMPP client state event changes and auto-trigger reconnection
|
||||
@@ -47,8 +63,10 @@ func NewStreamManager(client StreamClient, pc PostConnect) *StreamManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Start launch the connection loop
|
||||
func (sm *StreamManager) Start() error {
|
||||
// Run launches the connection of the underlying client or component
|
||||
// and wait until Disconnect is called, or for the manager to terminate due
|
||||
// to an unrecoverable error.
|
||||
func (sm *StreamManager) Run() error {
|
||||
if sm.client == nil {
|
||||
return errors.New("missing stream client")
|
||||
}
|
||||
@@ -61,7 +79,7 @@ func (sm *StreamManager) Start() error {
|
||||
sm.Metrics.setLoginTime()
|
||||
case StateDisconnected:
|
||||
// Reconnect on disconnection
|
||||
sm.connect()
|
||||
sm.resume(e.SMState)
|
||||
case StateStreamError:
|
||||
sm.client.Disconnect()
|
||||
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
|
||||
@@ -72,7 +90,13 @@ func (sm *StreamManager) Start() error {
|
||||
}
|
||||
sm.client.SetHandler(handler)
|
||||
|
||||
return sm.connect()
|
||||
sm.wg.Add(1)
|
||||
if err := sm.connect(); err != nil {
|
||||
sm.wg.Done()
|
||||
return err
|
||||
}
|
||||
sm.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop cancels pending operations and terminates existing XMPP client.
|
||||
@@ -80,25 +104,31 @@ func (sm *StreamManager) Stop() {
|
||||
// Remove on disconnect handler to avoid triggering reconnect
|
||||
sm.client.SetHandler(nil)
|
||||
sm.client.Disconnect()
|
||||
sm.wg.Done()
|
||||
}
|
||||
|
||||
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
||||
func (sm *StreamManager) connect() error {
|
||||
var backoff Backoff // TODO: Group backoff calculation features with connection manager?
|
||||
var state SMState
|
||||
return sm.resume(state)
|
||||
}
|
||||
|
||||
// resume manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
||||
func (sm *StreamManager) resume(state SMState) error {
|
||||
var backoff backoff // TODO: Group backoff calculation features with connection manager?
|
||||
|
||||
for {
|
||||
var err error
|
||||
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
||||
sm.Metrics = initMetrics()
|
||||
|
||||
if err = sm.client.Connect(); err != nil {
|
||||
if err = sm.client.Resume(state); err != nil {
|
||||
var actualErr ConnError
|
||||
if xerrors.As(err, &actualErr) {
|
||||
if actualErr.Permanent {
|
||||
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
|
||||
return xerrors.Errorf("unrecoverable connect error %#v", actualErr)
|
||||
}
|
||||
}
|
||||
backoff.Wait()
|
||||
backoff.wait()
|
||||
} else { // We are connected, we can leave the retry loop
|
||||
break
|
||||
}
|
||||
@@ -122,7 +152,7 @@ type Metrics struct {
|
||||
ConnectTime time.Duration
|
||||
// LoginTime returns the between client initiation of the TCP/IP
|
||||
// connection to the server and the return of the login result.
|
||||
// This includes ConnectTime, but also XMPP level protocol negociation
|
||||
// This includes ConnectTime, but also XMPP level protocol negotiation
|
||||
// like starttls.
|
||||
LoginTime time.Duration
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package xmpp // import "gosrc.io/xmpp"
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
29
xmpp_test.go
29
xmpp_test.go
@@ -1,29 +0,0 @@
|
||||
package xmpp_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// Compare iq structure but ignore empty namespace as they are set properly on
|
||||
// marshal / unmarshal. There is no need to manage them on the manually
|
||||
// crafted structure.
|
||||
func xmlEqual(x, y interface{}) bool {
|
||||
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
|
||||
opts := cmp.Options{
|
||||
cmp.FilterValues(func(x, y interface{}) bool {
|
||||
xx, xok := x.(xml.Name)
|
||||
yy, yok := y.(xml.Name)
|
||||
if xok && yok {
|
||||
zero := xml.Name{}
|
||||
if xx == zero || yy == zero {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, alwaysEqual),
|
||||
}
|
||||
|
||||
return cmp.Equal(x, y, opts)
|
||||
}
|
||||
Reference in New Issue
Block a user