mirror of
https://github.com/FluuxIO/go-xmpp.git
synced 2025-11-07 02:33:48 -08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e92089f96 | ||
|
|
7850d07d37 | ||
|
|
477a2b114c | ||
|
|
7a932d0504 | ||
|
|
eff622df76 | ||
|
|
5fcb1c4337 | ||
|
|
e59a86c380 | ||
|
|
0459144512 | ||
|
|
22ba8d1f4e | ||
|
|
fa59efbfdc | ||
|
|
086ceb4047 | ||
|
|
35e3defc62 | ||
|
|
79cd7e37f1 | ||
|
|
2083cbf29c | ||
|
|
928c1595ef | ||
|
|
70ef1d575f | ||
|
|
8798ff6fc1 | ||
|
|
3a3a15507e | ||
|
|
84665d8c13 | ||
|
|
e9bda893d6 | ||
|
|
1d1adb0c48 | ||
|
|
20e02cc9ad | ||
|
|
9b557a68b3 | ||
|
|
9ca9f48c89 | ||
|
|
6b0a036d07 | ||
|
|
f3218c4afa |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,5 +1,21 @@
|
||||
# Fluux XMPP Changelog
|
||||
|
||||
## v0.5.0
|
||||
|
||||
### Changes
|
||||
|
||||
- Added support for XEP-0198 (Stream management)
|
||||
- Added message queue : when using "SendX" methods on a client, messages are also stored in a queue. When requesting
|
||||
acks from the server, sent messages will be discarded, and unsent ones will be sent again. (see https://xmpp.org/extensions/xep-0198.html#acking)
|
||||
- Added support for stanza_errors (see https://xmpp.org/rfcs/rfc3920.html#def C.2. Stream error namespace and https://xmpp.org/rfcs/rfc6120.html#schemas-streamerror)
|
||||
- Added separate hooks for connection and reconnection on the client. One can now specify different actions to get triggered on client connect
|
||||
and reconnect, at client init time.
|
||||
- Client state update is now thread safe
|
||||
- Changed the Config struct to use pointer semantics
|
||||
- Tests
|
||||
- Refactoring, including removing some Fprintf statements in favor of Marshal + Write and using structs from the library
|
||||
instead of strings
|
||||
|
||||
## v0.4.0
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -11,7 +11,7 @@ The goal is to make simple to write simple XMPP clients and components:
|
||||
- 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.
|
||||
The library is designed to have minimal dependencies. Currently it requires at least Go 1.13.
|
||||
|
||||
## Configuration and connection
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"})
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
payload := CustomPayload{XMLName: xml.Name{Space: "my:custom:payload", Local: "query"}, Node: "test"}
|
||||
iq.Payload = payload
|
||||
|
||||
@@ -44,6 +47,9 @@ func (c CustomPayload) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
func init() {
|
||||
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
|
||||
func (c CustomPayload) GetSet() *stanza.ResultSet {
|
||||
return nil
|
||||
}
|
||||
func init() {
|
||||
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "my:custom:payload", Local: "query"}, CustomPayload{})
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ func main() {
|
||||
IQNamespaces("urn:xmpp:delegation:1").
|
||||
HandlerFunc(handleDelegation)
|
||||
|
||||
component, err := xmpp.NewComponent(opts, router)
|
||||
component, err := xmpp.NewComponent(opts, router, func(err error) {
|
||||
log.Println(err)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
@@ -78,7 +80,7 @@ const (
|
||||
// ctx.Opts
|
||||
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(stanza.IQ)
|
||||
iq, ok := p.(*stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -87,15 +89,18 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create IQ response: %v", err)
|
||||
}
|
||||
|
||||
switch info.Node {
|
||||
case "":
|
||||
discoInfoRoot(&iqResp, opts)
|
||||
discoInfoRoot(iqResp, opts)
|
||||
case pubsubNode:
|
||||
discoInfoPubSub(&iqResp)
|
||||
discoInfoPubSub(iqResp)
|
||||
case pepNode:
|
||||
discoInfoPEP(&iqResp)
|
||||
discoInfoPEP(iqResp)
|
||||
}
|
||||
|
||||
_ = c.Send(iqResp)
|
||||
@@ -155,7 +160,7 @@ func discoInfoPEP(iqResp *stanza.IQ) {
|
||||
|
||||
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(stanza.IQ)
|
||||
iq, ok := p.(*stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -166,7 +171,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||
}
|
||||
forwardedPacket := delegation.Forwarded.Stanza
|
||||
fmt.Println(forwardedPacket)
|
||||
forwardedIQ, ok := forwardedPacket.(stanza.IQ)
|
||||
forwardedIQ, ok := forwardedPacket.(*stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -179,7 +184,10 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||
|
||||
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})
|
||||
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create iqResp: %v", err)
|
||||
}
|
||||
payload := stanza.PubSubGeneric{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://jabber.org/protocol/pubsub",
|
||||
@@ -188,7 +196,10 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||
}
|
||||
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})
|
||||
iqForward, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create iqForward: %v", err)
|
||||
}
|
||||
delegPayload := stanza.Delegation{
|
||||
XMLName: xml.Name{
|
||||
Space: "urn:xmpp:delegation:1",
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.13
|
||||
require (
|
||||
github.com/processone/mpg123 v1.0.0
|
||||
github.com/processone/soundcloud v1.0.0
|
||||
gosrc.io/xmpp v0.1.1
|
||||
gosrc.io/xmpp v0.4.0
|
||||
)
|
||||
|
||||
replace gosrc.io/xmpp => ./../
|
||||
|
||||
@@ -99,7 +99,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA=
|
||||
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
||||
github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE=
|
||||
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
||||
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=
|
||||
@@ -155,6 +157,7 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/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/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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=
|
||||
|
||||
@@ -6,5 +6,5 @@ require (
|
||||
github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.6.1
|
||||
gosrc.io/xmpp v0.3.1-0.20191223080939-f8f820170e08
|
||||
gosrc.io/xmpp v0.4.0
|
||||
)
|
||||
|
||||
@@ -186,7 +186,7 @@ func startClient(g *gocui.Gui, config *config) {
|
||||
|
||||
// ==========================
|
||||
// Start working
|
||||
updateRosterFromConfig(g, config)
|
||||
updateRosterFromConfig(config)
|
||||
// Sending the default contact in a channel. Default value is the first contact in the list from the config.
|
||||
viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0]
|
||||
// Informing user of the default contact
|
||||
@@ -283,7 +283,7 @@ func errorHandler(err error) {
|
||||
|
||||
// Read the client roster from the config. This does not check with the server that the roster is correct.
|
||||
// If user tries to send a message to someone not registered with the server, the server will return an error.
|
||||
func updateRosterFromConfig(g *gocui.Gui, config *config) {
|
||||
func updateRosterFromConfig(config *config) {
|
||||
viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts)
|
||||
// Put a "go back" button at the end of the list
|
||||
viewState.contacts = append(viewState.contacts, backFromContacts)
|
||||
|
||||
@@ -61,12 +61,16 @@ func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||
|
||||
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(stanza.IQ)
|
||||
iq, ok := p.(*stanza.IQ)
|
||||
if !ok || iq.Type != stanza.IQTypeGet {
|
||||
return
|
||||
}
|
||||
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
// TODO: fix this...
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
disco := iqResp.DiscoInfo()
|
||||
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
@@ -76,7 +80,7 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
// TODO: Handle iq error responses
|
||||
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(stanza.IQ)
|
||||
iq, ok := p.(*stanza.IQ)
|
||||
if !ok || iq.Type != stanza.IQTypeGet {
|
||||
return
|
||||
}
|
||||
@@ -86,7 +90,11 @@ func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||
return
|
||||
}
|
||||
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
// TODO: fix this...
|
||||
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
items := iqResp.DiscoItems()
|
||||
|
||||
if discoItems.Node == "" {
|
||||
@@ -97,12 +105,15 @@ func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||
|
||||
func handleVersion(c xmpp.Sender, p stanza.Packet) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(stanza.IQ)
|
||||
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, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
|
||||
_ = c.Send(iqResp)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ Connect to an XMPP server using XEP 114 protocol, perform a discovery query on t
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
xmpp "gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
@@ -53,10 +54,13 @@ func main() {
|
||||
}
|
||||
|
||||
// make a disco iq
|
||||
iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet,
|
||||
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet,
|
||||
From: domain,
|
||||
To: "localhost",
|
||||
Id: "my-iq1"})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
disco := iqReq.DiscoInfo()
|
||||
iqReq.Payload = disco
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func main() {
|
||||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func main() {
|
||||
handleIQ(s, p, player)
|
||||
})
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||
}
|
||||
|
||||
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||
iq, ok := p.(stanza.IQ)
|
||||
iq, ok := p.(*stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||
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)
|
||||
_ = s.Send(&reply)
|
||||
// TODO add Soundclound artist / title retrieval
|
||||
sendUserTune(s, "Radiohead", "Spectre")
|
||||
default:
|
||||
|
||||
@@ -28,7 +28,7 @@ func main() {
|
||||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
17
_examples/xmpp_pubsub_client/README.md
Normal file
17
_examples/xmpp_pubsub_client/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# PubSub client example
|
||||
|
||||
## Description
|
||||
This is a simple example of a client that :
|
||||
* Creates a node on a service
|
||||
* Subscribes to that node
|
||||
* Publishes to that node
|
||||
* Gets the notification from the publication and prints it on screen
|
||||
|
||||
## Requirements
|
||||
You need to have a running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html).
|
||||
|
||||
## How to use
|
||||
Just run :
|
||||
```
|
||||
go run xmpp_ps_client.go
|
||||
```
|
||||
278
_examples/xmpp_pubsub_client/xmpp_ps_client.go
Normal file
278
_examples/xmpp_pubsub_client/xmpp_ps_client.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
userJID = "testuser2@localhost"
|
||||
serverAddress = "localhost:5222"
|
||||
nodeName = "lel_node"
|
||||
serviceName = "pubsub.localhost"
|
||||
)
|
||||
|
||||
var invalidResp = errors.New("invalid response")
|
||||
|
||||
func main() {
|
||||
|
||||
config := xmpp.Config{
|
||||
TransportConfiguration: xmpp.TransportConfiguration{
|
||||
Address: serverAddress,
|
||||
},
|
||||
Jid: userJID,
|
||||
Credential: xmpp.Password("pass123"),
|
||||
// StreamLogger: os.Stdout,
|
||||
Insecure: true,
|
||||
}
|
||||
router := xmpp.NewRouter()
|
||||
router.NewRoute().Packet("message").
|
||||
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||
data, _ := xml.Marshal(p)
|
||||
log.Println("Received a message ! => \n" + string(data))
|
||||
})
|
||||
|
||||
client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) })
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// Client connection
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// Create a node
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
createNode(ctx, cancel, client)
|
||||
|
||||
// ================================================================================
|
||||
// Configure the node. This can also be done in a single message with the creation
|
||||
configureNode(ctx, cancel, client)
|
||||
|
||||
// ====================================
|
||||
// Subscribe to this node :
|
||||
subToNode(ctx, cancel, client)
|
||||
|
||||
// ==========================
|
||||
// Publish to that node
|
||||
pubToNode(ctx, cancel, client)
|
||||
|
||||
// =============================
|
||||
// Let's purge the node :
|
||||
purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName)
|
||||
purgeCh, err := client.SendIQ(ctx, purgeRq)
|
||||
if err != nil {
|
||||
log.Fatalf("could not send purge request: %v", err)
|
||||
}
|
||||
select {
|
||||
case purgeResp := <-purgeCh:
|
||||
|
||||
if purgeResp.Type == stanza.IQTypeError {
|
||||
cancel()
|
||||
if vld, err := purgeResp.IsValid(); !vld {
|
||||
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", purgeResp, err)
|
||||
}
|
||||
log.Fatalf("error while purging node : %s", purgeResp.Error.Text)
|
||||
}
|
||||
log.Println("node successfully purged")
|
||||
case <-time.After(1000 * time.Millisecond):
|
||||
cancel()
|
||||
log.Fatal("No iq response was received in time while purging node")
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
func createNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||
rqCreate, err := stanza.NewCreateNode(serviceName, nodeName)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
createCh, err := client.SendIQ(ctx, rqCreate)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
} else {
|
||||
|
||||
if createCh != nil {
|
||||
select {
|
||||
case respCr := <-createCh:
|
||||
// Got response from server
|
||||
if respCr.Type == stanza.IQTypeError {
|
||||
if vld, err := respCr.IsValid(); !vld {
|
||||
log.Fatalf(invalidResp.Error()+" %+v"+" reason: %s", respCr, err)
|
||||
}
|
||||
if respCr.Error.Reason != "conflict" {
|
||||
log.Fatalf("%+v", respCr.Error.Text)
|
||||
}
|
||||
log.Println(respCr.Error.Text)
|
||||
} else {
|
||||
fmt.Print("successfully created channel")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
cancel()
|
||||
log.Fatal("No iq response was received in time while creating node")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configureNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||
// First, ask for a form with the config options
|
||||
confRq, _ := stanza.NewConfigureNode(serviceName, nodeName)
|
||||
confReqCh, err := client.SendIQ(ctx, confRq)
|
||||
if err != nil {
|
||||
log.Fatalf("could not send iq : %v", err)
|
||||
}
|
||||
select {
|
||||
case confForm := <-confReqCh:
|
||||
// If the request was successful, we now have a form with configuration options to update
|
||||
fields, err := confForm.GetFormFields()
|
||||
if err != nil {
|
||||
log.Fatal("No config fields found !")
|
||||
}
|
||||
|
||||
// These are some common fields expected to be present. Change processing to your liking
|
||||
if fields["pubsub#max_payload_size"] != nil {
|
||||
fields["pubsub#max_payload_size"].ValuesList[0] = "100000"
|
||||
}
|
||||
|
||||
if fields["pubsub#notification_type"] != nil {
|
||||
fields["pubsub#notification_type"].ValuesList[0] = "headline"
|
||||
}
|
||||
|
||||
// Send the modified fields as a form
|
||||
submitConf, err := stanza.NewFormSubmissionOwner(serviceName,
|
||||
nodeName,
|
||||
[]*stanza.Field{
|
||||
fields["pubsub#max_payload_size"],
|
||||
fields["pubsub#notification_type"],
|
||||
})
|
||||
|
||||
c, _ := client.SendIQ(ctx, submitConf)
|
||||
select {
|
||||
case confResp := <-c:
|
||||
if confResp.Type == stanza.IQTypeError {
|
||||
cancel()
|
||||
if vld, err := confResp.IsValid(); !vld {
|
||||
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", confResp, err)
|
||||
}
|
||||
log.Fatalf("node configuration failed : %s", confResp.Error.Text)
|
||||
}
|
||||
log.Println("node configuration was successful")
|
||||
return
|
||||
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
cancel()
|
||||
log.Fatal("No iq response was received in time while configuring the node")
|
||||
}
|
||||
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
cancel()
|
||||
log.Fatal("No iq response was received in time while asking for the config form")
|
||||
}
|
||||
}
|
||||
|
||||
func subToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||
rqSubscribe, err := stanza.NewSubRq(serviceName, stanza.SubInfo{
|
||||
Node: nodeName,
|
||||
Jid: userJID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
subRespCh, _ := client.SendIQ(ctx, rqSubscribe)
|
||||
if subRespCh != nil {
|
||||
select {
|
||||
case <-subRespCh:
|
||||
log.Println("Subscribed to the service")
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
cancel()
|
||||
log.Fatal("No iq response was received in time while subscribing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pubToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||
pub, err := stanza.NewPublishItemRq(serviceName, nodeName, "", stanza.Item{
|
||||
Publisher: "testuser2",
|
||||
Any: &stanza.Node{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://www.w3.org/2005/Atom",
|
||||
Local: "entry",
|
||||
},
|
||||
Nodes: []stanza.Node{
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "title"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item title",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "summary"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content summary",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "link"},
|
||||
Attrs: []xml.Attr{
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "rel"},
|
||||
Value: "alternate",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "type"},
|
||||
Value: "text/html",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "href"},
|
||||
Value: "http://denmark.lit/2003/12/13/atom03",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "id"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content ID",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "published"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "updated"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
pubRespCh, _ := client.SendIQ(ctx, pub)
|
||||
if pubRespCh != nil {
|
||||
select {
|
||||
case <-pubRespCh:
|
||||
log.Println("Published item to the service")
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
cancel()
|
||||
log.Fatal("No iq response was received in time while publishing")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ func main() {
|
||||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
13
auth.go
13
auth.go
@@ -60,10 +60,21 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str
|
||||
raw := "\x00" + user + "\x00" + secret
|
||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||
_, err := fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='%s'>%s</auth>", stanza.NSSASL, mech, enc)
|
||||
|
||||
a := stanza.SASLAuth{
|
||||
Mechanism: mech,
|
||||
Value: string(enc),
|
||||
}
|
||||
data, err := xml.Marshal(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := socket.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if n == 0 {
|
||||
return errors.New("failed to write authSASL nonza to socket : wrote 0 bytes")
|
||||
}
|
||||
|
||||
// Next message should be either success or failure.
|
||||
val, err := stanza.NextPacket(decoder)
|
||||
|
||||
12
bi_dir_iterator.go
Normal file
12
bi_dir_iterator.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package xmpp
|
||||
|
||||
type BiDirIterator interface {
|
||||
// Next returns the next element of this iterator, if a response is available within t milliseconds
|
||||
Next(t int) (BiDirIteratorElt, error)
|
||||
// Previous returns the previous element of this iterator, if a response is available within t milliseconds
|
||||
Previous(t int) (BiDirIteratorElt, error)
|
||||
}
|
||||
|
||||
type BiDirIteratorElt interface {
|
||||
NoOp()
|
||||
}
|
||||
163
client.go
163
client.go
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
@@ -14,15 +15,36 @@ import (
|
||||
//=============================================================================
|
||||
// EventManager
|
||||
|
||||
// ConnState represents the current connection state.
|
||||
// SyncConnState represents the current connection state.
|
||||
type SyncConnState struct {
|
||||
sync.RWMutex
|
||||
// Current state of the client. Please use the dedicated getter and setter for this field as they are thread safe.
|
||||
state ConnState
|
||||
}
|
||||
type ConnState = uint8
|
||||
|
||||
// getState is a thread-safe getter for the current state
|
||||
func (scs *SyncConnState) getState() ConnState {
|
||||
var res ConnState
|
||||
scs.RLock()
|
||||
res = scs.state
|
||||
scs.RUnlock()
|
||||
return res
|
||||
}
|
||||
|
||||
// setState is a thread-safe setter for the current
|
||||
func (scs *SyncConnState) setState(cs ConnState) {
|
||||
scs.Lock()
|
||||
scs.state = cs
|
||||
scs.Unlock()
|
||||
}
|
||||
|
||||
// This is a the list of events happening on the connection that the
|
||||
// client can be notified about.
|
||||
const (
|
||||
InitialPresence = "<presence/>"
|
||||
StateDisconnected ConnState = iota
|
||||
StateConnected
|
||||
StateResuming
|
||||
StateSessionEstablished
|
||||
StateStreamError
|
||||
StatePermanentError
|
||||
@@ -31,7 +53,7 @@ const (
|
||||
// Event is a structure use to convey event changes related to client state. This
|
||||
// is for example used to notify the client when the client get disconnected.
|
||||
type Event struct {
|
||||
State ConnState
|
||||
State SyncConnState
|
||||
Description string
|
||||
StreamError string
|
||||
SMState SMState
|
||||
@@ -44,7 +66,16 @@ type SMState struct {
|
||||
Id string
|
||||
// Inbound stanza count
|
||||
Inbound uint
|
||||
// TODO Store location for IP affinity
|
||||
|
||||
// IP affinity
|
||||
preferredReconAddr string
|
||||
|
||||
// Error
|
||||
StreamErrorGroup stanza.StanzaErrorGroup
|
||||
|
||||
// Track sent stanzas
|
||||
*stanza.UnAckQueue
|
||||
|
||||
// TODO Store max and timestamp, to check if we should retry resumption or not
|
||||
}
|
||||
|
||||
@@ -53,29 +84,35 @@ type SMState struct {
|
||||
type EventHandler func(Event) error
|
||||
|
||||
type EventManager struct {
|
||||
// Store current state
|
||||
CurrentState ConnState
|
||||
// Store current state. Please use "getState" and "setState" to access and/or modify this.
|
||||
CurrentState SyncConnState
|
||||
|
||||
// Callback used to propagate connection state changes
|
||||
Handler EventHandler
|
||||
}
|
||||
|
||||
// updateState changes the CurrentState in the event manager. The state read is threadsafe but there is no guarantee
|
||||
// regarding the triggered callback function.
|
||||
func (em *EventManager) updateState(state ConnState) {
|
||||
em.CurrentState = state
|
||||
em.CurrentState.setState(state)
|
||||
if em.Handler != nil {
|
||||
em.Handler(Event{State: em.CurrentState})
|
||||
}
|
||||
}
|
||||
|
||||
// disconnected changes the CurrentState in the event manager to "disconnected". The state read is threadsafe but there is no guarantee
|
||||
// regarding the triggered callback function.
|
||||
func (em *EventManager) disconnected(state SMState) {
|
||||
em.CurrentState = StateDisconnected
|
||||
em.CurrentState.setState(StateDisconnected)
|
||||
if em.Handler != nil {
|
||||
em.Handler(Event{State: em.CurrentState, SMState: state})
|
||||
}
|
||||
}
|
||||
|
||||
// streamError changes the CurrentState in the event manager to "streamError". The state read is threadsafe but there is no guarantee
|
||||
// regarding the triggered callback function.
|
||||
func (em *EventManager) streamError(error, desc string) {
|
||||
em.CurrentState = StateStreamError
|
||||
em.CurrentState.setState(StateStreamError)
|
||||
if em.Handler != nil {
|
||||
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
|
||||
}
|
||||
@@ -90,7 +127,7 @@ var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ s
|
||||
// server.
|
||||
type Client struct {
|
||||
// Store user defined options and states
|
||||
config Config
|
||||
config *Config
|
||||
// Session gather data that can be accessed by users of this library
|
||||
Session *Session
|
||||
transport Transport
|
||||
@@ -100,6 +137,12 @@ type Client struct {
|
||||
EventManager
|
||||
// Handle errors from client execution
|
||||
ErrorHandler func(error)
|
||||
|
||||
// Post connection hook. This will be executed on first connection
|
||||
PostConnectHook func() error
|
||||
|
||||
// Post resume hook. This will be executed after the client resumes a lost connection using StreamManagement (XEP-0198)
|
||||
PostResumeHook func() error
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -107,9 +150,9 @@ 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.
|
||||
// If host is not specified, the DNS SRV should be used to find the host from the domain part of the Jid.
|
||||
// Default the port to 5222.
|
||||
func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) {
|
||||
func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, err error) {
|
||||
if config.KeepaliveInterval == 0 {
|
||||
config.KeepaliveInterval = time.Second * 30
|
||||
}
|
||||
@@ -169,26 +212,45 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e
|
||||
return
|
||||
}
|
||||
|
||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
||||
// Connect simply triggers resumption, with an empty session state.
|
||||
// Connect establishes a first time connection to a XMPP server.
|
||||
// It calls the PostConnectHook
|
||||
func (c *Client) Connect() error {
|
||||
var state SMState
|
||||
return c.Resume(state)
|
||||
err := c.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 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 ?
|
||||
err = c.sendWithWriter(c.transport, []byte(InitialPresence))
|
||||
// Execute the post first connection hook. Typically this holds "ask for roster" and this type of actions.
|
||||
if c.PostConnectHook != nil {
|
||||
err = c.PostConnectHook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the keepalive go routine
|
||||
keepaliveQuit := make(chan struct{})
|
||||
go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit)
|
||||
// Start the receiver go routine
|
||||
go c.recv(keepaliveQuit)
|
||||
return err
|
||||
}
|
||||
|
||||
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
||||
// state.
|
||||
func (c *Client) Resume(state SMState) error {
|
||||
// connect establishes an actual TCP connection, based on previously defined parameters, as well as a XMPP session
|
||||
func (c *Client) connect() error {
|
||||
var state SMState
|
||||
var err error
|
||||
|
||||
// This is the TCP connection
|
||||
streamId, err := c.transport.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
if c.Session, err = NewSession(c.transport, c.config, state); err != nil {
|
||||
// Client is ok, we now open XMPP session with TLS negotiation if possible and session resume or binding
|
||||
// depending on state.
|
||||
if c.Session, err = NewSession(c, state); err != nil {
|
||||
// Try to get the stream close tag from the server.
|
||||
go func() {
|
||||
for {
|
||||
@@ -212,22 +274,26 @@ func (c *Client) Resume(state SMState) error {
|
||||
c.Session.StreamId = streamId
|
||||
c.updateState(StateSessionEstablished)
|
||||
|
||||
// Start the keepalive go routine
|
||||
keepaliveQuit := make(chan struct{})
|
||||
go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit)
|
||||
// Start the receiver go routine
|
||||
state = c.Session.SMState
|
||||
go c.recv(state, keepaliveQuit)
|
||||
|
||||
// We're connected and can now receive and send messages.
|
||||
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
||||
// TODO: Do we always want to send initial presence automatically ?
|
||||
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
||||
err = c.sendWithWriter(c.transport, []byte(InitialPresence))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
||||
// state. See XEP-0198
|
||||
func (c *Client) Resume() error {
|
||||
c.EventManager.updateState(StateResuming)
|
||||
err := c.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrival
|
||||
// for example.
|
||||
if c.PostResumeHook != nil {
|
||||
err = c.PostResumeHook()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Disconnect disconnects the client from the server, sending a stream close nonza and closing the TCP connection.
|
||||
func (c *Client) Disconnect() error {
|
||||
if c.transport != nil {
|
||||
return c.transport.Close()
|
||||
@@ -252,6 +318,15 @@ func (c *Client) Send(packet stanza.Packet) error {
|
||||
return errors.New("cannot marshal packet " + err.Error())
|
||||
}
|
||||
|
||||
// Store stanza as non-acked as part of stream management
|
||||
// See https://xmpp.org/extensions/xep-0198.html#scenarios
|
||||
if c.config.StreamManagementEnable {
|
||||
if _, ok := packet.(stanza.SMRequest); !ok {
|
||||
toStore := stanza.UnAckedStz{Stz: string(data)}
|
||||
c.Session.SMState.UnAckQueue.Push(&toStore)
|
||||
}
|
||||
}
|
||||
|
||||
return c.sendWithWriter(c.transport, data)
|
||||
}
|
||||
|
||||
@@ -264,7 +339,7 @@ func (c *Client) Send(packet stanza.Packet) error {
|
||||
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||
// result := <- client.SendIQ(ctx, iq)
|
||||
//
|
||||
func (c *Client) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
|
||||
func (c *Client) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
|
||||
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||
return nil, ErrCanOnlySendGetOrSetIq
|
||||
}
|
||||
@@ -284,6 +359,12 @@ func (c *Client) SendRaw(packet string) error {
|
||||
return errors.New("client is not connected")
|
||||
}
|
||||
|
||||
// Store stanza as non-acked as part of stream management
|
||||
// See https://xmpp.org/extensions/xep-0198.html#scenarios
|
||||
if c.config.StreamManagementEnable {
|
||||
toStore := stanza.UnAckedStz{Stz: packet}
|
||||
c.Session.SMState.UnAckQueue.Push(&toStore)
|
||||
}
|
||||
return c.sendWithWriter(c.transport, []byte(packet))
|
||||
}
|
||||
|
||||
@@ -297,13 +378,13 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||
// Go routines
|
||||
|
||||
// Loop: Receive data from server
|
||||
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) {
|
||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) {
|
||||
for {
|
||||
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||
if err != nil {
|
||||
c.ErrorHandler(err)
|
||||
close(keepaliveQuit)
|
||||
c.disconnected(state)
|
||||
c.disconnected(c.Session.SMState)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -321,7 +402,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) {
|
||||
answer := stanza.SMAnswer{XMLName: xml.Name{
|
||||
Space: stanza.NSStreamManagement,
|
||||
Local: "a",
|
||||
}, H: state.Inbound}
|
||||
}, H: c.Session.SMState.Inbound}
|
||||
err = c.Send(answer)
|
||||
if err != nil {
|
||||
c.ErrorHandler(err)
|
||||
@@ -332,7 +413,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) {
|
||||
c.transport.ReceivedStreamClose()
|
||||
return
|
||||
default:
|
||||
state.Inbound++
|
||||
c.Session.SMState.Inbound++
|
||||
}
|
||||
// Do normal route processing in a go-routine so we can immediately
|
||||
// start receiving other stanzas. This also allows route handlers to
|
||||
|
||||
@@ -2,7 +2,16 @@ package xmpp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
streamManagementID = "test-stream_management-id"
|
||||
)
|
||||
|
||||
func TestClient_Send(t *testing.T) {
|
||||
@@ -17,3 +26,583 @@ func TestClient_Send(t *testing.T) {
|
||||
t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Stream management test.
|
||||
// Connection is established, then the server sends supported features and so on.
|
||||
// After the bind, client attempts a stream management enablement, and server replies in kind.
|
||||
func Test_StreamManagement(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, true)
|
||||
go func() {
|
||||
var state SMState
|
||||
var err error
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
waitForEntity(t, serverDone)
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
// Absence of stream management test.
|
||||
// Connection is established, then the server sends supported features and so on.
|
||||
// Client has stream management disabled in its config, and should not ask for it. Server is not set up to reply.
|
||||
func Test_NoStreamManagement(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
// Setup Mock server
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, false)
|
||||
|
||||
go func() {
|
||||
var state SMState
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
var err error
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
waitForEntity(t, serverDone)
|
||||
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementNotSupported(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, true)
|
||||
|
||||
go func() {
|
||||
var state SMState
|
||||
var err error
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
// Wait for client
|
||||
waitForEntity(t, clientDone)
|
||||
|
||||
// Check if client got a positive stream management response from the server
|
||||
if client.Session.Features.DoesStreamManagement() {
|
||||
t.Fatalf("server does not provide stream management")
|
||||
}
|
||||
|
||||
// Wait for server
|
||||
waitForEntity(t, serverDone)
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementNoResume(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, false)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, true)
|
||||
|
||||
go func() {
|
||||
var state SMState
|
||||
var err error
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
waitForEntity(t, clientDone)
|
||||
if IsStreamResumable(client) {
|
||||
t.Fatalf("server does not support resumption but client says stream is resumable")
|
||||
}
|
||||
waitForEntity(t, serverDone)
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementResume(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
discardPresence(t, sc)
|
||||
serverDone <- struct{}{}
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Connect client, then disconnect it so we can resume the session
|
||||
go func() {
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
|
||||
// ===========================================================================================
|
||||
// Check that the client correctly went into "disconnected" state, after being disconnected
|
||||
statusCorrectChan := make(chan struct{})
|
||||
kill := make(chan struct{})
|
||||
|
||||
transp, ok := client.transport.(*XMPPTransport)
|
||||
if !ok {
|
||||
t.Fatalf("problem with client transport ")
|
||||
}
|
||||
|
||||
transp.conn.Close()
|
||||
|
||||
waitForEntity(t, serverDone)
|
||||
mock.Stop()
|
||||
|
||||
go checkClientResumeStatus(client, statusCorrectChan, kill)
|
||||
select {
|
||||
case <-statusCorrectChan:
|
||||
// Test passed
|
||||
case <-time.After(5 * time.Second):
|
||||
kill <- struct{}{}
|
||||
t.Fatalf("Client is not in disconnected state while it should be. Timed out")
|
||||
}
|
||||
|
||||
// Check if the client can have its connection resumed using its state but also its configuration
|
||||
if !IsStreamResumable(client) {
|
||||
t.Fatalf("should support resumption")
|
||||
}
|
||||
|
||||
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
|
||||
// and they should be different between a first connection and a stream resume since exchanged messages
|
||||
// are different (See XEP-0198)
|
||||
mock2 := ServerMock{}
|
||||
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
// Reconnect
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
resumeStream(t, sc)
|
||||
serverDone <- struct{}{}
|
||||
})
|
||||
|
||||
// Reconnect
|
||||
go func() {
|
||||
err = client.Resume()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
waitForEntity(t, serverDone)
|
||||
|
||||
mock2.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementFail(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, true, true)
|
||||
serverDone <- struct{}{}
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
var state SMState
|
||||
go func() {
|
||||
_, err = client.transport.Connect()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err == nil {
|
||||
t.Fatalf("test is supposed to err")
|
||||
}
|
||||
if client.Session.SMState.StreamErrorGroup == nil {
|
||||
t.Fatalf("error was not stored correctly in session state")
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, serverDone)
|
||||
waitForEntity(t, clientDone)
|
||||
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_SendStanzaQueueWithSM(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
|
||||
// Ignore the initial presence sent to the server by the client so we can move on to the next packet.
|
||||
discardPresence(t, sc)
|
||||
|
||||
// Used here to silently discard the IQ sent by the client, in order to later trigger a resend
|
||||
skipPacket(t, sc)
|
||||
// Respond to the client ACK request with a number of processed stanzas of 0. This should trigger a resend
|
||||
// of previously ignored stanza to the server, which this handler element will be expecting.
|
||||
respondWithAck(t, sc, 0)
|
||||
serverDone <- struct{}{}
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = client.Connect()
|
||||
|
||||
client.SendRaw(`<iq id='ls72g593' type='get'>
|
||||
<query xmlns='jabber:iq:roster'/>
|
||||
</iq>
|
||||
`)
|
||||
|
||||
// Last stanza was discarded silently by the server. Let's ask an ack for it. This should trigger resend as the server
|
||||
// will respond with an acknowledged number of stanzas of 0.
|
||||
r := stanza.SMRequest{}
|
||||
client.Send(r)
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
waitForEntity(t, serverDone)
|
||||
waitForEntity(t, clientDone)
|
||||
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
//========================================================================
|
||||
// Helper functions for tests
|
||||
|
||||
func skipPacket(t *testing.T, sc *ServerConn) {
|
||||
var p stanza.IQ
|
||||
se, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read packet: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
|
||||
t.Fatalf("cannot decode packet: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func respondWithAck(t *testing.T, sc *ServerConn, h int) {
|
||||
|
||||
// Mock server reads the ack request
|
||||
var p stanza.SMRequest
|
||||
se, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read packet: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
|
||||
t.Fatalf("cannot decode packet: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Mock server sends the ack response
|
||||
a := stanza.SMAnswer{
|
||||
H: uint(h),
|
||||
}
|
||||
data, err := xml.Marshal(a)
|
||||
_, err = sc.connection.Write(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to send response ack")
|
||||
}
|
||||
|
||||
// Mock server reads the re-sent stanza that was previously discarded intentionally
|
||||
var p2 stanza.IQ
|
||||
nse, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read packet: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&p2, &nse); err != nil {
|
||||
t.Fatalf("cannot decode packet: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func sendFeaturesStreamManagment(t *testing.T, sc *ServerConn) {
|
||||
// This is a basic server, supporting only 2 features after auth: stream management & session binding
|
||||
features := `<stream:features>
|
||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||
<sm xmlns='urn:xmpp:sm:3'/>
|
||||
</stream:features>`
|
||||
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||
t.Fatalf("cannot send stream feature: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func sendFeaturesNoStreamManagment(t *testing.T, sc *ServerConn) {
|
||||
// This is a basic server, supporting only 2 features after auth: stream management & session binding
|
||||
features := `<stream:features>
|
||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||
</stream:features>`
|
||||
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||
t.Fatalf("cannot send stream feature: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// enableStreamManagement is a function for the mock server that can either mock a successful session, or fail depending on
|
||||
// the value of the "fail" boolean. True means the session should fail.
|
||||
func enableStreamManagement(t *testing.T, sc *ServerConn, fail bool, resume bool) {
|
||||
// Decode element into pointer storage
|
||||
var ed stanza.SMEnable
|
||||
se, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read stream management enable: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&ed, &se); err != nil {
|
||||
t.Fatalf("cannot decode stream management enable: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if fail {
|
||||
f := stanza.SMFailed{
|
||||
H: nil,
|
||||
StreamErrorGroup: &stanza.UnexpectedRequest{},
|
||||
}
|
||||
data, err := xml.Marshal(f)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshall error response: %s", err)
|
||||
}
|
||||
sc.connection.Write(data)
|
||||
} else {
|
||||
e := &stanza.SMEnabled{
|
||||
Resume: strconv.FormatBool(resume),
|
||||
Id: streamManagementID,
|
||||
}
|
||||
data, err := xml.Marshal(e)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshall error response: %s", err)
|
||||
}
|
||||
sc.connection.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func resumeStream(t *testing.T, sc *ServerConn) {
|
||||
h := uint(0)
|
||||
response := stanza.SMResumed{
|
||||
PrevId: streamManagementID,
|
||||
H: &h,
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshall stream management enabled response : %s", err)
|
||||
}
|
||||
|
||||
writtenChan := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
sc.connection.Write(data)
|
||||
writtenChan <- struct{}{}
|
||||
}()
|
||||
select {
|
||||
case <-writtenChan:
|
||||
// We're done here
|
||||
return
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatalf("failed to write enabled nonza to client")
|
||||
}
|
||||
}
|
||||
|
||||
func checkClientResumeStatus(client *Client, statusCorrectChan chan struct{}, killChan chan struct{}) {
|
||||
for {
|
||||
if client.CurrentState.getState() == StateDisconnected {
|
||||
statusCorrectChan <- struct{}{}
|
||||
}
|
||||
select {
|
||||
case <-killChan:
|
||||
return
|
||||
case <-time.After(time.Millisecond * 10):
|
||||
// Keep checking status value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initSrvCliForResumeTests(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int, StreamManagementEnable, StreamManagementResume bool) (*Client, *ServerMock) {
|
||||
mock := &ServerMock{}
|
||||
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
|
||||
|
||||
mock.Start(t, testServerAddress, serverHandler)
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testServerAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: StreamManagementEnable,
|
||||
streamManagementResume: StreamManagementResume}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Fatalf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
if _, err = client.transport.Connect(); err != nil {
|
||||
t.Fatalf("XMPP connection failed: %s", err)
|
||||
}
|
||||
|
||||
return client, mock
|
||||
}
|
||||
|
||||
func waitForEntity(t *testing.T, entityDone chan struct{}) {
|
||||
select {
|
||||
case <-entityDone:
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatalf("test timed out")
|
||||
}
|
||||
}
|
||||
|
||||
204
client_test.go
204
client_test.go
@@ -20,18 +20,20 @@ const (
|
||||
|
||||
func TestEventManager(t *testing.T) {
|
||||
mgr := EventManager{}
|
||||
mgr.updateState(StateConnected)
|
||||
if mgr.CurrentState != StateConnected {
|
||||
mgr.updateState(StateResuming)
|
||||
if mgr.CurrentState.getState() != StateResuming {
|
||||
t.Fatal("CurrentState not updated by updateState()")
|
||||
}
|
||||
|
||||
mgr.disconnected(SMState{})
|
||||
if mgr.CurrentState != StateDisconnected {
|
||||
|
||||
if mgr.CurrentState.getState() != StateDisconnected {
|
||||
t.Fatalf("CurrentState not reset by disconnected()")
|
||||
}
|
||||
|
||||
mgr.streamError(ErrTLSNotSupported.Error(), "")
|
||||
if mgr.CurrentState != StateStreamError {
|
||||
|
||||
if mgr.CurrentState.getState() != StateStreamError {
|
||||
t.Fatalf("CurrentState not set by streamError()")
|
||||
}
|
||||
}
|
||||
@@ -53,7 +55,7 @@ func TestClient_Connect(t *testing.T) {
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -84,7 +86,7 @@ func TestClient_NoInsecure(t *testing.T) {
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -147,7 +149,7 @@ func TestClient_RFC3921Session(t *testing.T) {
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -171,7 +173,11 @@ func TestClient_SendIQ(t *testing.T) {
|
||||
client, mock := mockClientConnection(t, h, testClientIqPort)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create the IQ request: %v", err)
|
||||
}
|
||||
|
||||
disco := iqReq.DiscoInfo()
|
||||
iqReq.Payload = disco
|
||||
|
||||
@@ -219,7 +225,10 @@ func TestClient_SendIQFail(t *testing.T) {
|
||||
//==================
|
||||
// Create an IQ to send
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ request: %v", err)
|
||||
}
|
||||
disco := iqReq.DiscoInfo()
|
||||
iqReq.Payload = disco
|
||||
// Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified
|
||||
@@ -359,7 +368,7 @@ func TestClient_DisconnectStreamManager(t *testing.T) {
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
@@ -379,6 +388,162 @@ func TestClient_DisconnectStreamManager(t *testing.T) {
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_ClientPostConnectHook(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
// Handler for Mock server
|
||||
h := func(t *testing.T, sc *ServerConn) {
|
||||
handlerClientConnectSuccess(t, sc)
|
||||
done <- struct{}{}
|
||||
}
|
||||
|
||||
hookChan := make(chan struct{})
|
||||
mock := &ServerMock{}
|
||||
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, testClientPostConnectHook)
|
||||
|
||||
mock.Start(t, testServerAddress, h)
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testServerAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
// The post connection client hook should just write to a channel that we will read later.
|
||||
client.PostConnectHook = func() error {
|
||||
go func() {
|
||||
hookChan <- struct{}{}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
// Handle a possible error
|
||||
errChan := make(chan error)
|
||||
errorHandler := func(err error) {
|
||||
errChan <- err
|
||||
}
|
||||
client.ErrorHandler = errorHandler
|
||||
if err = client.Connect(); err != nil {
|
||||
t.Errorf("XMPP connection failed: %s", err)
|
||||
}
|
||||
|
||||
// Check if the post connection client hook was correctly called
|
||||
select {
|
||||
case err := <-errChan: // If the server sends an error, or there is a connection error
|
||||
t.Fatal(err.Error())
|
||||
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||
t.Fatal("Failed to call post connection client hook")
|
||||
case <-hookChan:
|
||||
// Test succeeded, channel was written to.
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
mock.Stop()
|
||||
case <-time.After(defaultChannelTimeout):
|
||||
t.Fatal("The mock server failed to finish its job !")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ClientPostReconnectHook(t *testing.T) {
|
||||
hookChan := make(chan struct{})
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
client.PostResumeHook = func() error {
|
||||
go func() {
|
||||
hookChan <- struct{}{}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
|
||||
transp, ok := client.transport.(*XMPPTransport)
|
||||
if !ok {
|
||||
t.Fatalf("problem with client transport ")
|
||||
}
|
||||
|
||||
transp.conn.Close()
|
||||
mock.Stop()
|
||||
|
||||
// Check if the client can have its connection resumed using its state but also its configuration
|
||||
if !IsStreamResumable(client) {
|
||||
t.Fatalf("should support resumption")
|
||||
}
|
||||
|
||||
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
|
||||
// and they should be different between a first connection and a stream resume since exchanged messages
|
||||
// are different (See XEP-0198)
|
||||
mock2 := ServerMock{}
|
||||
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
// Reconnect
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
resumeStream(t, sc)
|
||||
})
|
||||
|
||||
// Reconnect
|
||||
err = client.Resume()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||
t.Fatal("Failed to call post connection client hook")
|
||||
case <-hookChan:
|
||||
// Test succeeded, channel was written to.
|
||||
}
|
||||
|
||||
mock2.Stop()
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Basic XMPP Server Mock Handlers.
|
||||
|
||||
@@ -387,7 +552,7 @@ func handlerClientConnectSuccess(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
fmt.Fprintln(sc.connection, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendBindFeature(t, sc) // Send post auth features
|
||||
@@ -404,7 +569,7 @@ func closeConn(t *testing.T, sc *ServerConn) {
|
||||
}
|
||||
switch cls.(type) {
|
||||
case stanza.StreamClosePacket:
|
||||
fmt.Fprintf(sc.connection, stanza.StreamClose)
|
||||
sc.connection.Write([]byte(stanza.StreamClose))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -423,7 +588,7 @@ func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) {
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
fmt.Fprintln(sc.connection, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendRFC3921Feature(t, sc) // Send post auth features
|
||||
@@ -432,14 +597,17 @@ func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) {
|
||||
}
|
||||
|
||||
func checkClientOpenStream(t *testing.T, sc *ServerConn) {
|
||||
sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deadline: %v", err)
|
||||
}
|
||||
defer sc.connection.SetDeadline(time.Time{})
|
||||
|
||||
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
|
||||
var token xml.Token
|
||||
token, err := sc.decoder.Token()
|
||||
if err != nil {
|
||||
t.Errorf("cannot read next token: %s", err)
|
||||
t.Fatalf("cannot read next token: %s", err)
|
||||
}
|
||||
|
||||
switch elem := token.(type) {
|
||||
@@ -454,6 +622,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +631,6 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo
|
||||
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
|
||||
|
||||
mock.Start(t, testServerAddress, serverHandler)
|
||||
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testServerAddress,
|
||||
@@ -474,7 +642,7 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,17 @@ func sendxmpp(cmd *cobra.Command, args []string) {
|
||||
msgText := args[1]
|
||||
|
||||
var err error
|
||||
client, err := xmpp.NewClient(xmpp.Config{
|
||||
client, err := xmpp.NewClient(&xmpp.Config{
|
||||
TransportConfiguration: xmpp.TransportConfiguration{
|
||||
Address: viper.GetString("addr"),
|
||||
},
|
||||
Jid: viper.GetString("jid"),
|
||||
Credential: xmpp.Password(viper.GetString("password")),
|
||||
}, xmpp.NewRouter())
|
||||
},
|
||||
xmpp.NewRouter(),
|
||||
func(err error) {
|
||||
log.Println(err)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("error when starting xmpp client: %s", err)
|
||||
|
||||
23
cmd/go.sum
23
cmd/go.sum
@@ -6,6 +6,8 @@ github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2e
|
||||
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/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
|
||||
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
|
||||
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=
|
||||
@@ -38,6 +40,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
||||
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-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@@ -62,7 +65,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
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=
|
||||
@@ -73,6 +78,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
||||
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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
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=
|
||||
@@ -87,6 +93,8 @@ 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/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -97,6 +105,7 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
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=
|
||||
@@ -126,6 +135,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
|
||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
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=
|
||||
@@ -139,16 +150,21 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
@@ -196,6 +212,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -203,6 +220,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
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/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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=
|
||||
@@ -219,14 +237,19 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||
|
||||
10
component.go
10
component.go
@@ -60,11 +60,10 @@ func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*
|
||||
// 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)
|
||||
return c.Resume()
|
||||
}
|
||||
|
||||
func (c *Component) Resume(sm SMState) error {
|
||||
func (c *Component) Resume() error {
|
||||
var err error
|
||||
var streamId string
|
||||
if c.ComponentOptions.TransportConfiguration.Domain == "" {
|
||||
@@ -73,16 +72,13 @@ func (c *Component) Resume(sm SMState) error {
|
||||
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||
if err != nil {
|
||||
c.updateState(StatePermanentError)
|
||||
|
||||
return NewConnError(err, true)
|
||||
}
|
||||
|
||||
if streamId, err = c.transport.Connect(); err != nil {
|
||||
c.updateState(StatePermanentError)
|
||||
|
||||
return NewConnError(err, true)
|
||||
}
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// Authentication
|
||||
if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("<handshake>%s</handshake>", c.handshake(streamId)))); err != nil {
|
||||
@@ -185,7 +181,7 @@ func (c *Component) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||
// result := <- client.SendIQ(ctx, iq)
|
||||
//
|
||||
func (c *Component) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
|
||||
func (c *Component) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
|
||||
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||
return nil, ErrCanOnlySendGetOrSetIq
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ func TestHandshake(t *testing.T) {
|
||||
// Tests connection process with a handshake exchange
|
||||
// Tests multiple session IDs. All serverConnections should generate a unique stream ID
|
||||
func TestGenerateHandshakeId(t *testing.T) {
|
||||
clientDone := make(chan struct{})
|
||||
serverDone := make(chan struct{})
|
||||
// Using this array with a channel to make a queue of values to test
|
||||
// These are stream IDs that will be used to test the connection process, mixing them with the "secret" to generate
|
||||
// some handshake value
|
||||
@@ -57,11 +59,10 @@ func TestGenerateHandshakeId(t *testing.T) {
|
||||
// Performs a Component connection with a handshake. It expects to have an ID sent its way through the "uchan"
|
||||
// channel of this file. Otherwise it will hang for ever.
|
||||
h := func(t *testing.T, sc *ServerConn) {
|
||||
|
||||
checkOpenStreamHandshakeID(t, sc, <-uchan)
|
||||
readHandshakeComponent(t, sc.decoder)
|
||||
fmt.Fprintln(sc.connection, "<handshake/>") // That's all the server needs to return (see xep-0114)
|
||||
return
|
||||
sc.connection.Write([]byte("<handshake/>")) // That's all the server needs to return (see xep-0114)
|
||||
serverDone <- struct{}{}
|
||||
}
|
||||
|
||||
// Init mock server
|
||||
@@ -92,14 +93,45 @@ func TestGenerateHandshakeId(t *testing.T) {
|
||||
}
|
||||
|
||||
// Try connecting, and storing the resulting streamID in a map.
|
||||
m := make(map[string]bool)
|
||||
for _, _ = range uuidsArray {
|
||||
streamId, _ := c.transport.Connect()
|
||||
m[c.handshake(streamId)] = true
|
||||
}
|
||||
if len(uuidsArray) != len(m) {
|
||||
t.Errorf("Handshake does not produce a unique id. Expected: %d unique ids, got: %d", len(uuidsArray), len(m))
|
||||
}
|
||||
go func() {
|
||||
m := make(map[string]bool)
|
||||
for range uuidsArray {
|
||||
idChan := make(chan string)
|
||||
go func() {
|
||||
streamId, err := c.transport.Connect()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to mock component connection to get a handshake: %s", err)
|
||||
}
|
||||
idChan <- streamId
|
||||
}()
|
||||
|
||||
var streamId string
|
||||
select {
|
||||
case streamId = <-idChan:
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatalf("test timed out")
|
||||
}
|
||||
|
||||
hs := stanza.Handshake{
|
||||
Value: c.handshake(streamId),
|
||||
}
|
||||
m[hs.Value] = true
|
||||
hsRaw, err := xml.Marshal(hs)
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal handshake: %s", err)
|
||||
}
|
||||
c.SendRaw(string(hsRaw))
|
||||
waitForEntity(t, serverDone)
|
||||
c.transport.Close()
|
||||
}
|
||||
if len(uuidsArray) != len(m) {
|
||||
t.Errorf("Handshake does not produce a unique id. Expected: %d unique ids, got: %d", len(uuidsArray), len(m))
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
// Test that NewStreamManager can accept a Component.
|
||||
@@ -121,17 +153,21 @@ func TestDecoder(t *testing.T) {
|
||||
|
||||
// Tests sending an IQ to the server, and getting the response
|
||||
func TestSendIq(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
h := func(t *testing.T, sc *ServerConn) {
|
||||
handlerForComponentIQSend(t, sc)
|
||||
done <- struct{}{}
|
||||
serverDone <- struct{}{}
|
||||
}
|
||||
|
||||
//Connecting to a mock server, initialized with given port and handler function
|
||||
c, m := mockComponentConnection(t, testSendIqPort, h)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ request: %v", err)
|
||||
}
|
||||
disco := iqReq.DiscoInfo()
|
||||
iqReq.Payload = disco
|
||||
|
||||
@@ -142,24 +178,23 @@ func TestSendIq(t *testing.T) {
|
||||
}
|
||||
c.ErrorHandler = errorHandler
|
||||
|
||||
var res chan stanza.IQ
|
||||
res, _ = c.SendIQ(ctx, iqReq)
|
||||
go func() {
|
||||
var res chan stanza.IQ
|
||||
res, _ = c.SendIQ(ctx, iqReq)
|
||||
|
||||
select {
|
||||
case <-res:
|
||||
case err := <-errChan:
|
||||
t.Errorf(err.Error())
|
||||
case <-time.After(defaultChannelTimeout):
|
||||
t.Errorf("Failed to receive response, to sent IQ, from mock server")
|
||||
}
|
||||
select {
|
||||
case <-res:
|
||||
case err := <-errChan:
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
waitForEntity(t, serverDone)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
m.Stop()
|
||||
case <-time.After(defaultChannelTimeout):
|
||||
t.Errorf("The mock server failed to finish its job !")
|
||||
}
|
||||
cancel()
|
||||
m.Stop()
|
||||
}
|
||||
|
||||
// Checking that error handling is done properly client side when an invalid IQ is sent and the server responds in kind.
|
||||
@@ -173,7 +208,10 @@ func TestSendIqFail(t *testing.T) {
|
||||
c, m := mockComponentConnection(t, testSendIqFailPort, h)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ request: %v", err)
|
||||
}
|
||||
|
||||
// Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified
|
||||
// so we need to overwrite it.
|
||||
@@ -395,7 +433,10 @@ func handlerForComponentIQSend(t *testing.T, sc *ServerConn) {
|
||||
|
||||
// Used for ID and handshake related tests
|
||||
func checkOpenStreamHandshakeID(t *testing.T, sc *ServerConn, streamID string) {
|
||||
sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deadline: %v", err)
|
||||
}
|
||||
defer sc.connection.SetDeadline(time.Time{})
|
||||
|
||||
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
|
||||
@@ -435,7 +476,10 @@ func handlerComponentFailedHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||
Body: "Fail my handshake.",
|
||||
}
|
||||
s, _ := xml.Marshal(me)
|
||||
fmt.Fprintln(sc.connection, string(s))
|
||||
_, err := sc.connection.Write(s)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write message: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -463,6 +507,6 @@ func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) {
|
||||
func handlerForComponentHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||
checkOpenStreamHandshakeDefaultID(t, sc)
|
||||
readHandshakeComponent(t, sc.decoder)
|
||||
fmt.Fprintln(sc.connection, "<handshake/>") // That's all the server needs to return (see xep-0114)
|
||||
sc.connection.Write([]byte("<handshake/>")) // That's all the server needs to return (see xep-0114)
|
||||
return
|
||||
}
|
||||
|
||||
11
config.go
11
config.go
@@ -21,4 +21,15 @@ type Config struct {
|
||||
// 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
|
||||
|
||||
// Activate stream management process during session
|
||||
StreamManagementEnable bool
|
||||
// Enable stream management resume capability
|
||||
streamManagementResume bool
|
||||
}
|
||||
|
||||
// IsStreamResumable tells if a stream session is resumable by reading the "config" part of a client.
|
||||
// It checks if stream management is enabled, and if stream resumption was set and accepted by the server.
|
||||
func IsStreamResumable(c *Client) bool {
|
||||
return c.config.StreamManagementEnable && c.config.streamManagementResume
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -3,11 +3,8 @@ module gosrc.io/xmpp
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/awesome-gocui/gocui v0.6.0 // indirect
|
||||
github.com/google/go-cmp v0.3.1
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/spf13/viper v1.6.1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||
nhooyr.io/websocket v1.6.5
|
||||
|
||||
)
|
||||
|
||||
50
router.go
50
router.go
@@ -42,7 +42,18 @@ func NewRouter() *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) {
|
||||
iq, isIq := p.(stanza.IQ)
|
||||
a, isA := p.(stanza.SMAnswer)
|
||||
if isA {
|
||||
switch tt := s.(type) {
|
||||
case *Client:
|
||||
lastAcked := a.H
|
||||
SendMissingStz(int(lastAcked), s, tt.Session.SMState.UnAckQueue)
|
||||
case *Component:
|
||||
// TODO
|
||||
default:
|
||||
}
|
||||
}
|
||||
iq, isIq := p.(*stanza.IQ)
|
||||
if isIq {
|
||||
r.IQResultRouteLock.RLock()
|
||||
route, ok := r.IQResultRoutes[iq.Id]
|
||||
@@ -51,7 +62,7 @@ func (r *Router) route(s Sender, p stanza.Packet) {
|
||||
r.IQResultRouteLock.Lock()
|
||||
delete(r.IQResultRoutes, iq.Id)
|
||||
r.IQResultRouteLock.Unlock()
|
||||
route.result <- iq
|
||||
route.result <- *iq
|
||||
close(route.result)
|
||||
return
|
||||
}
|
||||
@@ -70,7 +81,34 @@ func (r *Router) route(s Sender, p stanza.Packet) {
|
||||
}
|
||||
}
|
||||
|
||||
func iqNotImplemented(s Sender, iq stanza.IQ) {
|
||||
// SendMissingStz sends all stanzas that did not reach the server, according to the response to an ack request (see XEP-0198, acks)
|
||||
func SendMissingStz(lastSent int, s Sender, uaq *stanza.UnAckQueue) error {
|
||||
uaq.RWMutex.Lock()
|
||||
if len(uaq.Uslice) <= 0 {
|
||||
uaq.RWMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
last := uaq.Uslice[len(uaq.Uslice)-1]
|
||||
if last.Id > lastSent {
|
||||
// Remove sent stanzas from the queue
|
||||
uaq.PopN(lastSent - last.Id)
|
||||
// Re-send non acknowledged stanzas
|
||||
for _, elt := range uaq.PopN(len(uaq.Uslice)) {
|
||||
eltStz := elt.(*stanza.UnAckedStz)
|
||||
err := s.SendRaw(eltStz.Stz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
// Ask for updates on stanzas we just sent to the entity. Not sure I should leave this. Maybe let users call ack again by themselves ?
|
||||
s.Send(stanza.SMRequest{})
|
||||
}
|
||||
uaq.RWMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func iqNotImplemented(s Sender, iq *stanza.IQ) {
|
||||
err := stanza.Err{
|
||||
XMLName: xml.Name{Local: "error"},
|
||||
Code: 501,
|
||||
@@ -232,7 +270,7 @@ func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
switch p.(type) {
|
||||
case stanza.Message:
|
||||
name = "message"
|
||||
case stanza.IQ:
|
||||
case *stanza.IQ:
|
||||
name = "iq"
|
||||
case stanza.Presence:
|
||||
name = "presence"
|
||||
@@ -259,7 +297,7 @@ type nsTypeMatcher []string
|
||||
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
var stanzaType stanza.StanzaType
|
||||
switch packet := p.(type) {
|
||||
case stanza.IQ:
|
||||
case *stanza.IQ:
|
||||
stanzaType = packet.Type
|
||||
case stanza.Presence:
|
||||
stanzaType = packet.Type
|
||||
@@ -291,7 +329,7 @@ func (r *Route) StanzaType(types ...string) *Route {
|
||||
type nsIQMatcher []string
|
||||
|
||||
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
iq, ok := p.(stanza.IQ)
|
||||
iq, ok := p.(*stanza.IQ)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ func TestIQResultRoutes(t *testing.T) {
|
||||
// Check if the IQ handler was called
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||
defer cancel()
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, Id: "1234"})
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, Id: "1234"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
res := router.NewIQResultRoute(ctx, "1234")
|
||||
go router.route(conn, iq)
|
||||
select {
|
||||
@@ -71,7 +74,10 @@ func TestNameMatcher(t *testing.T) {
|
||||
|
||||
// Check that an IQ packet is not matched
|
||||
conn = NewSenderMock()
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iq.Payload = &stanza.DiscoInfo{}
|
||||
router.route(conn, iq)
|
||||
if conn.String() == successFlag {
|
||||
@@ -89,7 +95,10 @@ func TestIQNSMatcher(t *testing.T) {
|
||||
|
||||
// Check that an IQ with proper namespace does match
|
||||
conn := NewSenderMock()
|
||||
iqDisco := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
iqDisco, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create iqDisco: %v", err)
|
||||
}
|
||||
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||
iqDisco.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
@@ -103,7 +112,10 @@ func TestIQNSMatcher(t *testing.T) {
|
||||
|
||||
// Check that another namespace is not matched
|
||||
conn = NewSenderMock()
|
||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create iqVersion: %v", err)
|
||||
}
|
||||
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||
iqVersion.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
@@ -146,7 +158,10 @@ func TestTypeMatcher(t *testing.T) {
|
||||
|
||||
// We do not match on other types
|
||||
conn = NewSenderMock()
|
||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create iqVersion: %v", err)
|
||||
}
|
||||
iqVersion.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
@@ -169,22 +184,31 @@ func TestCompositeMatcher(t *testing.T) {
|
||||
})
|
||||
|
||||
// Data set
|
||||
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
getVersionIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create getVersionIq: %v", err)
|
||||
}
|
||||
getVersionIq.Payload = &stanza.Version{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
Local: "query",
|
||||
}}
|
||||
|
||||
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
setVersionIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create setVersionIq: %v", err)
|
||||
}
|
||||
setVersionIq.Payload = &stanza.Version{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
Local: "query",
|
||||
}}
|
||||
|
||||
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
GetDiscoIq.Payload = &stanza.DiscoInfo{
|
||||
getDiscoIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create getDiscoIq: %v", err)
|
||||
}
|
||||
getDiscoIq.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://jabber.org/protocol/disco#info",
|
||||
Local: "query",
|
||||
@@ -200,7 +224,7 @@ func TestCompositeMatcher(t *testing.T) {
|
||||
}{
|
||||
{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 get discoinfo iq", input: getDiscoIq, want: false},
|
||||
{name: "ignore message", input: message, want: false},
|
||||
}
|
||||
|
||||
@@ -238,7 +262,10 @@ func TestCatchallMatcher(t *testing.T) {
|
||||
}
|
||||
|
||||
conn = NewSenderMock()
|
||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create iqVersion: %v", err)
|
||||
}
|
||||
iqVersion.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
@@ -274,7 +301,7 @@ func (s SenderMock) Send(packet stanza.Packet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SenderMock) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
|
||||
func (s SenderMock) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
|
||||
out, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
183
session.go
183
session.go
@@ -1,10 +1,11 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
@@ -23,44 +24,67 @@ type Session struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewSession(transport Transport, o Config, state SMState) (*Session, error) {
|
||||
s := new(Session)
|
||||
s.transport = transport
|
||||
s.SMState = state
|
||||
s.init(o)
|
||||
func NewSession(c *Client, state SMState) (*Session, error) {
|
||||
var s *Session
|
||||
if c.Session == nil {
|
||||
s = new(Session)
|
||||
s.transport = c.transport
|
||||
s.SMState = state
|
||||
s.init()
|
||||
} else {
|
||||
s = c.Session
|
||||
// We keep information about the previously set session, like the session ID, but we read server provided
|
||||
// info again in case it changed between session break and resume, such as features.
|
||||
s.init()
|
||||
}
|
||||
|
||||
if s.err != nil {
|
||||
return nil, NewConnError(s.err, true)
|
||||
}
|
||||
|
||||
if !transport.IsSecure() {
|
||||
s.startTlsIfSupported(o)
|
||||
if !c.transport.IsSecure() {
|
||||
s.startTlsIfSupported(c.config)
|
||||
}
|
||||
|
||||
if !transport.IsSecure() && !o.Insecure {
|
||||
if !c.transport.IsSecure() && !c.config.Insecure {
|
||||
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
||||
return nil, NewConnError(err, true)
|
||||
}
|
||||
|
||||
if s.TlsEnabled {
|
||||
s.reset(o)
|
||||
s.reset()
|
||||
}
|
||||
|
||||
// auth
|
||||
s.auth(o)
|
||||
s.reset(o)
|
||||
s.auth(c.config)
|
||||
if s.err != nil {
|
||||
return s, s.err
|
||||
}
|
||||
s.reset()
|
||||
if s.err != nil {
|
||||
return s, s.err
|
||||
}
|
||||
|
||||
// attempt resumption
|
||||
if s.resume(o) {
|
||||
if s.resume(c.config) {
|
||||
return s, s.err
|
||||
}
|
||||
|
||||
// otherwise, bind resource and 'start' XMPP session
|
||||
s.bind(o)
|
||||
s.rfc3921Session(o)
|
||||
s.bind(c.config)
|
||||
if s.err != nil {
|
||||
return s, s.err
|
||||
}
|
||||
s.rfc3921Session()
|
||||
if s.err != nil {
|
||||
return s, s.err
|
||||
}
|
||||
|
||||
// Enable stream management if supported
|
||||
s.EnableStreamManagement(o)
|
||||
s.EnableStreamManagement(c.config)
|
||||
if s.err != nil {
|
||||
return s, s.err
|
||||
}
|
||||
|
||||
return s, s.err
|
||||
}
|
||||
@@ -70,19 +94,20 @@ func (s *Session) PacketId() string {
|
||||
return fmt.Sprintf("%x", s.lastPacketId)
|
||||
}
|
||||
|
||||
func (s *Session) init(o Config) {
|
||||
s.Features = s.open(o.parsedJid.Domain)
|
||||
// init gathers information on the session such as stream features from the server.
|
||||
func (s *Session) init() {
|
||||
s.Features = s.extractStreamFeatures()
|
||||
}
|
||||
|
||||
func (s *Session) reset(o Config) {
|
||||
func (s *Session) reset() {
|
||||
if s.StreamId, s.err = s.transport.StartStream(); s.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.Features = s.open(o.parsedJid.Domain)
|
||||
s.Features = s.extractStreamFeatures()
|
||||
}
|
||||
|
||||
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
||||
func (s *Session) extractStreamFeatures() (f stanza.StreamFeatures) {
|
||||
// extract stream features
|
||||
if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil {
|
||||
s.err = errors.New("stream open decode features: " + s.err.Error())
|
||||
@@ -90,14 +115,14 @@ func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) startTlsIfSupported(o Config) {
|
||||
func (s *Session) startTlsIfSupported(o *Config) {
|
||||
if s.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.transport.DoesStartTLS() {
|
||||
if !o.Insecure {
|
||||
s.err = errors.New("Transport does not support starttls")
|
||||
s.err = errors.New("transport does not support starttls")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -125,7 +150,7 @@ func (s *Session) startTlsIfSupported(o Config) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) auth(o Config) {
|
||||
func (s *Session) auth(o *Config) {
|
||||
if s.err != nil {
|
||||
return
|
||||
}
|
||||
@@ -134,7 +159,7 @@ func (s *Session) auth(o Config) {
|
||||
}
|
||||
|
||||
// Attempt to resume session using stream management
|
||||
func (s *Session) resume(o Config) bool {
|
||||
func (s *Session) resume(o *Config) bool {
|
||||
if !s.Features.DoesStreamManagement() {
|
||||
return false
|
||||
}
|
||||
@@ -142,9 +167,16 @@ func (s *Session) resume(o Config) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Fprintf(s.transport, "<resume xmlns='%s' h='%d' previd='%s'/>",
|
||||
stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id)
|
||||
rsm := stanza.SMResume{
|
||||
PrevId: s.SMState.Id,
|
||||
H: &s.SMState.Inbound,
|
||||
}
|
||||
data, err := xml.Marshal(rsm)
|
||||
|
||||
_, err = s.transport.Write(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var packet stanza.Packet
|
||||
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||
if s.err == nil {
|
||||
@@ -165,20 +197,48 @@ func (s *Session) resume(o Config) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Session) bind(o Config) {
|
||||
func (s *Session) bind(o *Config) {
|
||||
if s.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Send IQ message asking to bind to the local user name.
|
||||
var resource = o.parsedJid.Resource
|
||||
if resource != "" {
|
||||
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
||||
s.PacketId(), stanza.NSBind, resource)
|
||||
} else {
|
||||
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
||||
iqB, err := stanza.NewIQ(stanza.Attrs{
|
||||
Type: stanza.IQTypeSet,
|
||||
Id: s.PacketId(),
|
||||
})
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we already have a resource name, and include it in the request if so
|
||||
if resource != "" {
|
||||
iqB.Payload = &stanza.Bind{
|
||||
Resource: resource,
|
||||
}
|
||||
} else {
|
||||
iqB.Payload = &stanza.Bind{}
|
||||
|
||||
}
|
||||
|
||||
// Send the bind request IQ
|
||||
data, err := xml.Marshal(iqB)
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
n, err := s.transport.Write(data)
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
} else if n == 0 {
|
||||
s.err = errors.New("failed to write bind iq stanza to the server : wrote 0 bytes")
|
||||
return
|
||||
}
|
||||
|
||||
// Check the server response
|
||||
var iq stanza.IQ
|
||||
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
||||
@@ -197,7 +257,7 @@ func (s *Session) bind(o Config) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func (s *Session) rfc3921Session() {
|
||||
if s.err != nil {
|
||||
return
|
||||
}
|
||||
@@ -205,7 +265,29 @@ func (s *Session) rfc3921Session(o Config) {
|
||||
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.transport, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
|
||||
se, err := stanza.NewIQ(stanza.Attrs{
|
||||
Type: stanza.IQTypeSet,
|
||||
Id: s.PacketId(),
|
||||
})
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
se.Payload = &stanza.StreamSession{}
|
||||
data, err := xml.Marshal(se)
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
n, err := s.transport.Write(data)
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
} else if n == 0 {
|
||||
s.err = errors.New("there was a problem marshaling the session IQ : wrote 0 bytes to server")
|
||||
return
|
||||
}
|
||||
|
||||
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
||||
return
|
||||
@@ -214,28 +296,47 @@ func (s *Session) rfc3921Session(o Config) {
|
||||
}
|
||||
|
||||
// Enable stream management, with session resumption, if supported.
|
||||
func (s *Session) EnableStreamManagement(o Config) {
|
||||
func (s *Session) EnableStreamManagement(o *Config) {
|
||||
if s.err != nil {
|
||||
return
|
||||
}
|
||||
if !s.Features.DoesStreamManagement() {
|
||||
if !s.Features.DoesStreamManagement() || !o.StreamManagementEnable {
|
||||
return
|
||||
}
|
||||
q := stanza.NewUnAckQueue()
|
||||
ebleNonza := stanza.SMEnable{Resume: &o.streamManagementResume}
|
||||
pktStr, err := xml.Marshal(ebleNonza)
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
_, err = s.transport.Write(pktStr)
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(s.transport, "<enable xmlns='%s' resume='true'/>", stanza.NSStreamManagement)
|
||||
|
||||
var packet stanza.Packet
|
||||
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||
if s.err == nil {
|
||||
switch p := packet.(type) {
|
||||
case stanza.SMEnabled:
|
||||
s.SMState = SMState{Id: p.Id}
|
||||
// Server allows resumption or not using SMEnabled attribute "resume". We must read the server response
|
||||
// and update config accordingly
|
||||
b, err := strconv.ParseBool(p.Resume)
|
||||
if err != nil || !b {
|
||||
o.StreamManagementEnable = false
|
||||
}
|
||||
s.SMState = SMState{Id: p.Id, preferredReconAddr: p.Location}
|
||||
s.SMState.UnAckQueue = q
|
||||
case stanza.SMFailed:
|
||||
// TODO: Store error in SMState, for later inspection
|
||||
s.SMState = SMState{StreamErrorGroup: p.StreamErrorGroup}
|
||||
s.SMState.UnAckQueue = q
|
||||
s.err = errors.New("failed to establish session : " + s.SMState.StreamErrorGroup.GroupErrorName())
|
||||
default:
|
||||
s.err = errors.New("unexpected reply to SM enable")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
type Command struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"`
|
||||
|
||||
CommandElement CommandElement `xml:",any"`
|
||||
CommandElement CommandElement
|
||||
|
||||
BadAction *struct{} `xml:"bad-action,omitempty"`
|
||||
BadLocale *struct{} `xml:"bad-locale,omitempty"`
|
||||
@@ -38,12 +38,19 @@ type Command struct {
|
||||
SessionId string `xml:"sessionid,attr,omitempty"`
|
||||
Status string `xml:"status,attr,omitempty"`
|
||||
Lang string `xml:"lang,attr,omitempty"`
|
||||
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Command) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
func (c *Command) GetSet() *ResultSet {
|
||||
return c.ResultSet
|
||||
}
|
||||
|
||||
type CommandElement interface {
|
||||
Ref() string
|
||||
}
|
||||
@@ -68,6 +75,7 @@ type Note struct {
|
||||
func (n *Note) Ref() string {
|
||||
return "note"
|
||||
}
|
||||
func (f *Form) Ref() string { return "form" }
|
||||
|
||||
func (n *Node) Ref() string {
|
||||
return "node"
|
||||
@@ -111,22 +119,27 @@ func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
|
||||
case "affiliations":
|
||||
a := Actions{}
|
||||
d.DecodeElement(&a, &tt)
|
||||
err = d.DecodeElement(&a, &tt)
|
||||
c.CommandElement = &a
|
||||
case "configure":
|
||||
nt := Note{}
|
||||
d.DecodeElement(&nt, &tt)
|
||||
err = d.DecodeElement(&nt, &tt)
|
||||
c.CommandElement = &nt
|
||||
case "x":
|
||||
f := Form{}
|
||||
err = d.DecodeElement(&f, &tt)
|
||||
c.CommandElement = &f
|
||||
default:
|
||||
n := Node{}
|
||||
e := d.DecodeElement(&n, &tt)
|
||||
_ = e
|
||||
err = d.DecodeElement(&n, &tt)
|
||||
c.CommandElement = &n
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
@@ -134,3 +147,7 @@ func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/commands", Local: "command"}, Command{})
|
||||
}
|
||||
|
||||
@@ -7,34 +7,21 @@ import (
|
||||
)
|
||||
|
||||
func TestMarshalCommands(t *testing.T) {
|
||||
input := "<command xmlns=\"http://jabber.org/protocol/commands\" node=\"list\" sessionid=\"list:20020923T213616Z-700\" status=\"completed\"><x " +
|
||||
"xmlns=\"jabber:x:data\" type=\"result\"><title xmlns=\"jabber:x:data\">Available Servi" +
|
||||
"ces</title><reported xmlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" label=\"S" +
|
||||
"ervice\" var=\"service\"></field><field xmlns=\"jabber:x:data\" label=\"Single-User mo" +
|
||||
"de\" var=\"runlevel-1\"></field><field xmlns=\"jabber:x:data\" label=\"Non-Networked M" +
|
||||
"ulti-User mode\" var=\"runlevel-2\"></field><field xmlns=\"jabber:x:data\" label=\"Ful" +
|
||||
"l Multi-User mode\" var=\"runlevel-3\"></field><field xmlns=\"jabber:x:data\" label=\"" +
|
||||
"X-Window mode\" var=\"runlevel-5\"></field></reported><item xmlns=\"jabber:x:data\"><" +
|
||||
"field xmlns=\"jabber:x:data\" var=\"service\"><value xmlns=\"jabber:x:data\">httpd</va" +
|
||||
"lue></field><field xmlns=\"jabber:x:data\" var=\"runlevel-1\"><value xmlns=\"jabber:x" +
|
||||
":data\">off</value></field><field xmlns=\"jabber:x:data\" var=\"runlevel-2\"><value x" +
|
||||
"mlns=\"jabber:x:data\">off</value></field><field xmlns=\"jabber:x:data\" var=\"runlev" +
|
||||
"el-3\"><value xmlns=\"jabber:x:data\">on</value></field><field xmlns=\"jabber:x:data" +
|
||||
"\" var=\"runlevel-5\"><value xmlns=\"jabber:x:data\">on</value></field></item><item x" +
|
||||
"mlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" var=\"service\"><value xmlns=\"ja" +
|
||||
"bber:x:data\">postgresql</value></field><field xmlns=\"jabber:x:data\" var=\"runleve" +
|
||||
"l-1\"><value xmlns=\"jabber:x:data\">off</value></field><field xmlns=\"jabber:x:data" +
|
||||
"\" var=\"runlevel-2\"><value xmlns=\"jabber:x:data\">off</value></field><field xmlns=" +
|
||||
"\"jabber:x:data\" var=\"runlevel-3\"><value xmlns=\"jabber:x:data\">on</value></field>" +
|
||||
"<field xmlns=\"jabber:x:data\" var=\"runlevel-5\"><value xmlns=\"jabber:x:data\">on</v" +
|
||||
"alue></field></item><item xmlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" var" +
|
||||
"=\"service\"><value xmlns=\"jabber:x:data\">jabberd</value></field><field xmlns=\"jab" +
|
||||
"ber:x:data\" var=\"runlevel-1\"><value xmlns=\"jabber:x:data\">off</value></field><fi" +
|
||||
"eld xmlns=\"jabber:x:data\" var=\"runlevel-2\"><value xmlns=\"jabber:x:data\">off</val" +
|
||||
"ue></field><field xmlns=\"jabber:x:data\" var=\"runlevel-3\"><value xmlns=\"jabber:x:" +
|
||||
"data\">on</value></field><field xmlns=\"jabber:x:data\" var=\"runlevel-5\"><value xml" +
|
||||
"ns=\"jabber:x:data\">on</value></field></item></x></command>"
|
||||
|
||||
input := "<command xmlns=\"http://jabber.org/protocol/commands\" node=\"list\" " +
|
||||
"sessionid=\"list:20020923T213616Z-700\" status=\"completed\"><x xmlns=\"jabber:x:data\" " +
|
||||
"type=\"result\"><title>Available Services</title><reported xmlns=\"jabber:x:data\"><field var=\"service\" " +
|
||||
"label=\"Service\"></field><field var=\"runlevel-1\" label=\"Single-User mode\">" +
|
||||
"</field><field var=\"runlevel-2\" label=\"Non-Networked Multi-User mode\"></field><field var=\"runlevel-3\" " +
|
||||
"label=\"Full Multi-User mode\"></field><field var=\"runlevel-5\" label=\"X-Window mode\"></field></reported>" +
|
||||
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>httpd</value></field><field var=\"runlevel-1\">" +
|
||||
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
|
||||
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
|
||||
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>postgresql</value></field>" +
|
||||
"<field var=\"runlevel-1\"><value>off</value></field><field var=\"runlevel-2\"><value>off</value></field>" +
|
||||
"<field var=\"runlevel-3\"><value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
|
||||
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>jabberd</value></field><field var=\"runlevel-1\">" +
|
||||
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
|
||||
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item></x></command>"
|
||||
var c stanza.Command
|
||||
err := xml.Unmarshal([]byte(input), &c)
|
||||
|
||||
|
||||
@@ -42,11 +42,16 @@ type Delegation struct {
|
||||
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
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Delegation) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
func (d *Delegation) GetSet() *ResultSet {
|
||||
return d.ResultSet
|
||||
}
|
||||
|
||||
// Forwarded is used to wrapped forwarded stanzas.
|
||||
// TODO: Move it in another file, as it is not limited to components.
|
||||
@@ -86,6 +91,6 @@ type Delegated struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestParsingDelegationIQ(t *testing.T) {
|
||||
if iq.Payload != nil {
|
||||
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||
packet := delegation.Forwarded.Stanza
|
||||
forwardedIQ, ok := packet.(IQ)
|
||||
forwardedIQ, ok := packet.(*IQ)
|
||||
if !ok {
|
||||
t.Errorf("Could not extract packet IQ")
|
||||
return
|
||||
|
||||
34
stanza/fifo_queue.go
Normal file
34
stanza/fifo_queue.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package stanza
|
||||
|
||||
// FIFO queue for string contents
|
||||
// Implementations have no guarantee regarding thread safety !
|
||||
type FifoQueue interface {
|
||||
// Pop returns the first inserted element still in queue and deletes it from queue. If queue is empty, returns nil
|
||||
// No guarantee regarding thread safety !
|
||||
Pop() Queueable
|
||||
|
||||
// PopN returns the N first inserted elements still in queue and deletes them from queue. If queue is empty or i<=0, returns nil
|
||||
// If number to pop is greater than queue length, returns all queue elements
|
||||
// No guarantee regarding thread safety !
|
||||
PopN(i int) []Queueable
|
||||
|
||||
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty, returns nil
|
||||
// No guarantee regarding thread safety !
|
||||
Peek() Queueable
|
||||
|
||||
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty or i<=0, returns nil.
|
||||
// If number to peek is greater than queue length, returns all queue elements
|
||||
// No guarantee regarding thread safety !
|
||||
PeekN() []Queueable
|
||||
// Push adds an element to the queue
|
||||
// No guarantee regarding thread safety !
|
||||
Push(s Queueable) error
|
||||
|
||||
// Empty returns true if queue is empty
|
||||
// No guarantee regarding thread safety !
|
||||
Empty() bool
|
||||
}
|
||||
|
||||
type Queueable interface {
|
||||
QueueableName() string
|
||||
}
|
||||
@@ -14,17 +14,18 @@ const (
|
||||
// See XEP-0004 and XEP-0068
|
||||
// Pointer semantics
|
||||
type Form struct {
|
||||
XMLName xml.Name `xml:"jabber:x:data x"`
|
||||
Instructions []string `xml:"instructions"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
Fields []Field `xml:"field,omitempty"`
|
||||
Reported *FormItem `xml:"reported"`
|
||||
Items []FormItem
|
||||
Type string `xml:"type,attr"`
|
||||
XMLName xml.Name `xml:"jabber:x:data x"`
|
||||
Instructions []string `xml:"instructions"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
Fields []*Field `xml:"field,omitempty"`
|
||||
Reported *FormItem `xml:"reported"`
|
||||
Items []FormItem `xml:"item,omitempty"`
|
||||
Type string `xml:"type,attr"`
|
||||
}
|
||||
|
||||
type FormItem struct {
|
||||
Fields []Field
|
||||
XMLName xml.Name
|
||||
Fields []Field `xml:"field,omitempty"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
@@ -38,7 +39,7 @@ type Field struct {
|
||||
Label string `xml:"label,attr,omitempty"`
|
||||
}
|
||||
|
||||
func NewForm(fields []Field, formType string) *Form {
|
||||
func NewForm(fields []*Field, formType string) *Form {
|
||||
return &Form{
|
||||
Type: formType,
|
||||
Fields: fields,
|
||||
|
||||
@@ -51,13 +51,16 @@ const (
|
||||
)
|
||||
|
||||
func TestMarshalFormSubmit(t *testing.T) {
|
||||
formIQ := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet})
|
||||
formIQ, err := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create formIQ: %v", err)
|
||||
}
|
||||
formIQ.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &ConfigureOwner{
|
||||
Node: serviceNode,
|
||||
Form: &Form{
|
||||
Type: FormTypeSubmit,
|
||||
Fields: []Field{
|
||||
Fields: []*Field{
|
||||
{Var: "FORM_TYPE", Type: FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}},
|
||||
{Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}},
|
||||
|
||||
@@ -7,12 +7,18 @@ import (
|
||||
type ControlSet struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
|
||||
Fields []ControlField `xml:",any"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ControlSet) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
func (c *ControlSet) GetSet() *ResultSet {
|
||||
return c.ResultSet
|
||||
}
|
||||
|
||||
type ControlGetForm struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
|
||||
}
|
||||
@@ -30,10 +36,13 @@ type ControlSetResponse struct {
|
||||
func (c *ControlSetResponse) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
func (c *ControlSetResponse) GetSet() *ResultSet {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:iot:control", Local: "set"}, ControlSet{})
|
||||
}
|
||||
|
||||
49
stanza/iq.go
49
stanza/iq.go
@@ -2,6 +2,7 @@ package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -31,22 +32,28 @@ type IQ struct { // Info/Query
|
||||
|
||||
type IQPayload interface {
|
||||
Namespace() string
|
||||
GetSet() *ResultSet
|
||||
}
|
||||
|
||||
func NewIQ(a Attrs) IQ {
|
||||
// TODO ensure that type is set, as it is required
|
||||
func NewIQ(a Attrs) (*IQ, error) {
|
||||
if a.Id == "" {
|
||||
if id, err := uuid.NewRandom(); err == nil {
|
||||
a.Id = id.String()
|
||||
}
|
||||
}
|
||||
return IQ{
|
||||
|
||||
iq := IQ{
|
||||
XMLName: xml.Name{Local: "iq"},
|
||||
Attrs: a,
|
||||
}
|
||||
|
||||
if iq.Type.IsEmpty() {
|
||||
return nil, IqTypeUnset
|
||||
}
|
||||
return &iq, nil
|
||||
}
|
||||
|
||||
func (iq IQ) MakeError(xerror Err) IQ {
|
||||
func (iq *IQ) MakeError(xerror Err) *IQ {
|
||||
from := iq.From
|
||||
to := iq.To
|
||||
|
||||
@@ -58,18 +65,23 @@ func (iq IQ) MakeError(xerror Err) IQ {
|
||||
return iq
|
||||
}
|
||||
|
||||
func (IQ) Name() string {
|
||||
func (*IQ) Name() string {
|
||||
return "iq"
|
||||
}
|
||||
|
||||
// NoOp to implement BiDirIteratorElt
|
||||
func (*IQ) NoOp() {
|
||||
|
||||
}
|
||||
|
||||
type iqDecoder struct{}
|
||||
|
||||
var iq iqDecoder
|
||||
|
||||
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
|
||||
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (*IQ, error) {
|
||||
var packet IQ
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
return &packet, err
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for IQs
|
||||
@@ -134,38 +146,47 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
IqTypeUnset = errors.New("iq type is not set but is mandatory")
|
||||
IqIDUnset = errors.New("iq stanza ID is not set but is mandatory")
|
||||
IqSGetNoPl = errors.New("iq is of type get or set but has no payload")
|
||||
IqResNoPl = errors.New("iq is of type result but has no payload")
|
||||
IqErrNoErrPl = errors.New("iq is of type error but has no error payload")
|
||||
)
|
||||
|
||||
// IsValid checks if the IQ is valid. If not, return an error with the reason as a message
|
||||
// Following RFC-3920 for IQs
|
||||
func (iq *IQ) IsValid() bool {
|
||||
func (iq *IQ) IsValid() (bool, error) {
|
||||
// ID is required
|
||||
if len(strings.TrimSpace(iq.Id)) == 0 {
|
||||
return false
|
||||
return false, IqIDUnset
|
||||
}
|
||||
|
||||
// Type is required
|
||||
if iq.Type.IsEmpty() {
|
||||
return false
|
||||
return false, IqTypeUnset
|
||||
}
|
||||
|
||||
// Type get and set must contain one and only one child element that specifies the semantics
|
||||
if iq.Type == IQTypeGet || iq.Type == IQTypeSet {
|
||||
if iq.Payload == nil && iq.Any == nil {
|
||||
return false
|
||||
return false, IqSGetNoPl
|
||||
}
|
||||
}
|
||||
|
||||
// A result must include zero or one child element
|
||||
if iq.Type == IQTypeResult {
|
||||
if iq.Payload != nil && iq.Any != nil {
|
||||
return false
|
||||
return false, IqResNoPl
|
||||
}
|
||||
}
|
||||
|
||||
//Error type must contain an "error" child element
|
||||
if iq.Type == IQTypeError {
|
||||
if iq.Error == nil {
|
||||
return false
|
||||
return false, IqErrNoErrPl
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ const (
|
||||
// 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"`
|
||||
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"`
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
// Namespace lets DiscoInfo implement the IQPayload interface
|
||||
@@ -27,6 +28,10 @@ func (d *DiscoInfo) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) GetSet() *ResultSet {
|
||||
return d.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
@@ -102,12 +107,19 @@ type DiscoItems struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Items []DiscoItem `xml:"item"`
|
||||
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (d *DiscoItems) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
func (d *DiscoItems) GetSet() *ResultSet {
|
||||
return d.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
@@ -146,6 +158,6 @@ type DiscoItem struct {
|
||||
// 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{Space: NSDiscoInfo, Local: "query"}, DiscoInfo{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoItems, Local: "query"}, DiscoItems{})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
|
||||
// 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"})
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
disco := iq.DiscoInfo()
|
||||
disco.AddIdentity("Test Component", "gateway", "service")
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
@@ -50,8 +53,11 @@ func TestDiscoInfo_Builder(t *testing.T) {
|
||||
// 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: stanza.IQTypeResult, From: "catalog.shakespeare.lit",
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit",
|
||||
To: "romeo@montague.net/orchard", Id: "items-2"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iq.DiscoItems().
|
||||
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
|
||||
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
|
||||
|
||||
@@ -35,12 +35,17 @@ const (
|
||||
// Roster struct represents Roster IQs
|
||||
type Roster struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:roster query"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
// Namespace defines the namespace for the RosterIQ
|
||||
func (r *Roster) Namespace() string {
|
||||
return r.XMLName.Space
|
||||
}
|
||||
func (r *Roster) GetSet() *ResultSet {
|
||||
return r.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
@@ -64,6 +69,8 @@ func (iq *IQ) RosterIQ() *Roster {
|
||||
type RosterItems struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:roster query"`
|
||||
Items []RosterItem `xml:"item"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
// Namespace lets RosterItems implement the IQPayload interface
|
||||
@@ -71,6 +78,10 @@ func (r *RosterItems) Namespace() string {
|
||||
return r.XMLName.Space
|
||||
}
|
||||
|
||||
func (r *RosterItems) GetSet() *ResultSet {
|
||||
return r.ResultSet
|
||||
}
|
||||
|
||||
// RosterItem represents an item in the roster iq
|
||||
type RosterItem struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:roster item"`
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
)
|
||||
|
||||
func TestRosterBuilder(t *testing.T) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
var noGroup []string
|
||||
|
||||
iq.RosterItems().AddItem("xl8ceawrfu8zdneomw1h6h28d@crypho.com",
|
||||
@@ -91,7 +94,7 @@ func TestRosterBuilder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkMarshalling(t *testing.T, iq IQ) (*IQ, error) {
|
||||
func checkMarshalling(t *testing.T, iq *IQ) (*IQ, error) {
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
|
||||
@@ -36,24 +36,36 @@ func TestUnmarshalIqs(t *testing.T) {
|
||||
|
||||
func TestGenerateIqId(t *testing.T) {
|
||||
t.Parallel()
|
||||
iq := stanza.NewIQ(stanza.Attrs{Id: "1"})
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Id: "1", Type: "dummy type"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
if iq.Id != "1" {
|
||||
t.Errorf("NewIQ replaced id with %s", iq.Id)
|
||||
}
|
||||
|
||||
iq = stanza.NewIQ(stanza.Attrs{})
|
||||
iq, err = stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
if iq.Id == "" {
|
||||
t.Error("NewIQ did not generate an Id")
|
||||
}
|
||||
|
||||
otherIq := stanza.NewIQ(stanza.Attrs{})
|
||||
otherIq, err := stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
if iq.Id == otherIq.Id {
|
||||
t.Errorf("NewIQ generated two identical ids: %s", iq.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateIq(t *testing.T) {
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
payload := stanza.DiscoInfo{
|
||||
Identity: []stanza.Identity{
|
||||
{Name: "Test Gateway",
|
||||
@@ -111,7 +123,10 @@ func TestErrorTag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDiscoItems(t *testing.T) {
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
payload := stanza.DiscoItems{
|
||||
Node: "music",
|
||||
}
|
||||
@@ -215,8 +230,9 @@ func TestIsValid(t *testing.T) {
|
||||
t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq)
|
||||
return
|
||||
}
|
||||
if !parsedIQ.IsValid() && !tcase.shouldErr {
|
||||
t.Errorf("failed iq validation for : %s", tcase.iq)
|
||||
isValid, err := parsedIQ.IsValid()
|
||||
if !isValid && !tcase.shouldErr {
|
||||
t.Errorf("failed validation for iq because: %s\nin test case : %s", err, tcase.iq)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,12 +11,18 @@ type Version struct {
|
||||
Name string `xml:"name,omitempty"`
|
||||
Version string `xml:"version,omitempty"`
|
||||
OS string `xml:"os,omitempty"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (v *Version) Namespace() string {
|
||||
return v.XMLName.Space
|
||||
}
|
||||
|
||||
func (v *Version) GetSet() *ResultSet {
|
||||
return v.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
@@ -41,5 +47,5 @@ func (v *Version) SetInfo(name, version, os string) *Version {
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "jabber:iq:version", Local: "query"}, Version{})
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ 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",
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
|
||||
To: "juliet@capulet.com/balcony", Id: "version_1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iq.Version().SetInfo(name, version, os)
|
||||
|
||||
parsedIQ, err := checkMarshalling(t, iq)
|
||||
|
||||
@@ -35,8 +35,8 @@ type MarkAcknowledged struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "markable"}, Markable{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "received"}, MarkReceived{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "displayed"}, MarkDisplayed{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "acknowledged"}, MarkAcknowledged{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "markable"}, Markable{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "received"}, MarkReceived{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "displayed"}, MarkDisplayed{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "acknowledged"}, MarkAcknowledged{})
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ type StatePaused struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "active"}, StateActive{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "composing"}, StateComposing{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "gone"}, StateGone{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "inactive"}, StateInactive{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "paused"}, StatePaused{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "active"}, StateActive{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "composing"}, StateComposing{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "gone"}, StateGone{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "inactive"}, StateInactive{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "paused"}, StatePaused{})
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@ type HTMLBody struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"http://jabber.org/protocol/xhtml-im", "html"}, HTML{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/xhtml-im", Local: "html"}, HTML{})
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@ type OOB struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"jabber:x:oob", "x"}, OOB{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "jabber:x:oob", Local: "x"}, OOB{})
|
||||
}
|
||||
|
||||
@@ -210,5 +210,4 @@ func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ type ReceiptReceived struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "request"}, ReceiptRequest{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "received"}, ReceiptReceived{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "request"}, ReceiptRequest{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "received"}, ReceiptReceived{})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
func TestNode_Marshal(t *testing.T) {
|
||||
jsonData := []byte("{\"key\":\"value\"}")
|
||||
|
||||
iqResp := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
iqResp, err := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iqResp.Any = &Node{
|
||||
XMLName: xml.Name{Space: "myNS", Local: "space"},
|
||||
Content: string(jsonData),
|
||||
|
||||
@@ -144,5 +144,5 @@ func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTPresence, xml.Name{"http://jabber.org/protocol/muc", "x"}, MucPresence{})
|
||||
TypeRegistry.MapExtension(PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc", Local: "x"}, MucPresence{})
|
||||
}
|
||||
|
||||
125
stanza/pubsub.go
125
stanza/pubsub.go
@@ -29,12 +29,19 @@ type PubSubGeneric struct {
|
||||
// To use in responses to sub/unsub for instance
|
||||
// Subscription options
|
||||
Unsubscribe *SubInfo `xml:"unsubscribe,omitempty"`
|
||||
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PubSubGeneric) Namespace() string {
|
||||
return p.XMLName.Space
|
||||
}
|
||||
|
||||
func (p *PubSubGeneric) GetSet() *ResultSet {
|
||||
return p.ResultSet
|
||||
}
|
||||
|
||||
type Affiliations struct {
|
||||
List []Affiliation `xml:"affiliation"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
@@ -156,12 +163,15 @@ type PubSubOption struct {
|
||||
// It's a Set type IQ.
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.1 Subscribe to a Node
|
||||
func NewSubRq(serviceId string, subInfo SubInfo) (IQ, error) {
|
||||
func NewSubRq(serviceId string, subInfo SubInfo) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return IQ{}, e
|
||||
return nil, e
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Subscribe: &subInfo,
|
||||
}
|
||||
@@ -172,12 +182,15 @@ func NewSubRq(serviceId string, subInfo SubInfo) (IQ, error) {
|
||||
// It's a Set type IQ
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.2 Unsubscribe from a Node
|
||||
func NewUnsubRq(serviceId string, subInfo SubInfo) (IQ, error) {
|
||||
func NewUnsubRq(serviceId string, subInfo SubInfo) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return IQ{}, e
|
||||
return nil, e
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Unsubscribe: &subInfo,
|
||||
}
|
||||
@@ -188,12 +201,15 @@ func NewUnsubRq(serviceId string, subInfo SubInfo) (IQ, error) {
|
||||
// It's a Get type IQ
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.3 Configure Subscription Options
|
||||
func NewSubOptsRq(serviceId string, subInfo SubInfo) (IQ, error) {
|
||||
func NewSubOptsRq(serviceId string, subInfo SubInfo) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return IQ{}, e
|
||||
return nil, e
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
SubOptions: &SubOptions{
|
||||
SubInfo: subInfo,
|
||||
@@ -205,15 +221,18 @@ func NewSubOptsRq(serviceId string, subInfo SubInfo) (IQ, error) {
|
||||
// NewFormSubmission builds a form submission pubsub IQ
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.3.5 Form Submission
|
||||
func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (IQ, error) {
|
||||
func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return IQ{}, e
|
||||
return nil, e
|
||||
}
|
||||
if form.Type != FormTypeSubmit {
|
||||
return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type)
|
||||
return nil, errors.New("form type was expected to be submit but was : " + form.Type)
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
SubOptions: &SubOptions{
|
||||
SubInfo: subInfo,
|
||||
@@ -229,14 +248,17 @@ func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (IQ, error
|
||||
// since the value of the <subscribe/> element's 'node' attribute specifies the desired NodeID and
|
||||
// the value of the <subscribe/> element's 'jid' attribute specifies the subscriber's JID
|
||||
// 6.3.7 Subscribe and Configure
|
||||
func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (IQ, error) {
|
||||
func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return IQ{}, e
|
||||
return nil, e
|
||||
}
|
||||
if form.Type != FormTypeSubmit {
|
||||
return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type)
|
||||
return nil, errors.New("form type was expected to be submit but was : " + form.Type)
|
||||
}
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Subscribe: &subInfo,
|
||||
SubOptions: &SubOptions{
|
||||
@@ -251,8 +273,11 @@ func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (IQ, error)
|
||||
// NewItemsRequest creates a request to query existing items from a node.
|
||||
// Specify a "maxItems" value to request only the last maxItems items. If 0, requests all items.
|
||||
// 6.5.2 Requesting All List AND 6.5.7 Requesting the Most Recent List
|
||||
func NewItemsRequest(serviceId string, node string, maxItems int) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewItemsRequest(serviceId string, node string, maxItems int) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Items: &Items{Node: node},
|
||||
}
|
||||
@@ -266,8 +291,11 @@ func NewItemsRequest(serviceId string, node string, maxItems int) (IQ, error) {
|
||||
|
||||
// NewItemsRequest creates a request to get a specific item from a node.
|
||||
// 6.5.8 Requesting a Particular Item
|
||||
func NewSpecificItemRequest(serviceId, node, itemId string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewSpecificItemRequest(serviceId, node, itemId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Items: &Items{Node: node,
|
||||
List: []Item{
|
||||
@@ -281,13 +309,16 @@ func NewSpecificItemRequest(serviceId, node, itemId string) (IQ, error) {
|
||||
}
|
||||
|
||||
// NewPublishItemRq creates a request to publish a single item to a node identified by its provided ID
|
||||
func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (IQ, error) {
|
||||
func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (*IQ, error) {
|
||||
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return IQ{}, errors.New("cannot publish without a target node ID")
|
||||
return nil, errors.New("cannot publish without a target node ID")
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Publish: &Publish{Node: nodeID, Items: []Item{item}},
|
||||
}
|
||||
@@ -306,13 +337,16 @@ func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (IQ, error
|
||||
// NewPublishItemOptsRq creates a request to publish items to a node identified by its provided ID, along with configuration options
|
||||
// A pubsub service MAY support the ability to specify options along with a publish request
|
||||
//(if so, it MUST advertise support for the "http://jabber.org/protocol/pubsub#publish-options" feature).
|
||||
func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (IQ, error) {
|
||||
func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (*IQ, error) {
|
||||
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return IQ{}, errors.New("cannot publish without a target node ID")
|
||||
return nil, errors.New("cannot publish without a target node ID")
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Publish: &Publish{Node: nodeID, Items: items},
|
||||
PublishOptions: options,
|
||||
@@ -324,13 +358,16 @@ func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *Publi
|
||||
// NewDelItemFromNode creates a request to delete and item from a node, given its id.
|
||||
// To delete an item, the publisher sends a retract request.
|
||||
// This helper function follows 7.2 Delete an Item from a Node
|
||||
func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (IQ, error) {
|
||||
func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (*IQ, error) {
|
||||
// "The <retract/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return IQ{}, errors.New("cannot delete item without a target node ID")
|
||||
return nil, errors.New("cannot delete item without a target node ID")
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Retract: &Retract{Node: nodeID, Items: []Item{{Id: itemId}}, Notify: notify},
|
||||
}
|
||||
@@ -339,8 +376,11 @@ func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (IQ, err
|
||||
|
||||
// NewCreateAndConfigNode makes a request for node creation that has the desired node configuration.
|
||||
// See 8.1.3 Create and Configure a Node
|
||||
func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Create: &Create{Node: nodeID},
|
||||
Configure: &Configure{Form: confForm},
|
||||
@@ -350,8 +390,11 @@ func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (IQ, error
|
||||
|
||||
// NewCreateNode builds a request to create a node on the service referenced by "serviceId"
|
||||
// See 8.1 Create a Node
|
||||
func NewCreateNode(serviceId, nodeName string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
func NewCreateNode(serviceId, nodeName string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Create: &Create{Node: nodeName},
|
||||
}
|
||||
@@ -361,8 +404,11 @@ func NewCreateNode(serviceId, nodeName string) (IQ, error) {
|
||||
// NewRetrieveAllSubsRequest builds a request to retrieve all subscriptions from all nodes
|
||||
// In order to make the request, the requesting entity MUST send an IQ-get whose <pubsub/>
|
||||
// child contains an empty <subscriptions/> element with no attributes.
|
||||
func NewRetrieveAllSubsRequest(serviceId string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewRetrieveAllSubsRequest(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Subscriptions: &Subscriptions{},
|
||||
}
|
||||
@@ -371,8 +417,11 @@ func NewRetrieveAllSubsRequest(serviceId string) (IQ, error) {
|
||||
|
||||
// NewRetrieveAllAffilsRequest builds a request to retrieve all affiliations from all nodes
|
||||
// In order to make the request of the service, the requesting entity includes an empty <affiliations/> element with no attributes.
|
||||
func NewRetrieveAllAffilsRequest(serviceId string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewRetrieveAllAffilsRequest(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Affiliations: &Affiliations{},
|
||||
}
|
||||
|
||||
@@ -9,12 +9,18 @@ import (
|
||||
type PubSubOwner struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"`
|
||||
OwnerUseCase OwnerUseCase
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (pso *PubSubOwner) Namespace() string {
|
||||
return pso.XMLName.Space
|
||||
}
|
||||
|
||||
func (pso *PubSubOwner) GetSet() *ResultSet {
|
||||
return pso.ResultSet
|
||||
}
|
||||
|
||||
type OwnerUseCase interface {
|
||||
UseCase() string
|
||||
}
|
||||
@@ -112,8 +118,11 @@ const (
|
||||
// NewConfigureNode creates a request to configure a node on the given service.
|
||||
// A form will be returned by the service, to which the user must respond using for instance the NewFormSubmission function.
|
||||
// See 8.2 Configure a Node
|
||||
func NewConfigureNode(serviceId, nodeName string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewConfigureNode(serviceId, nodeName string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &ConfigureOwner{Node: nodeName},
|
||||
}
|
||||
@@ -122,11 +131,14 @@ func NewConfigureNode(serviceId, nodeName string) (IQ, error) {
|
||||
|
||||
// NewDelNode creates a request to delete node "nodeID" from the "serviceId" service
|
||||
// See 8.4 Delete a Node
|
||||
func NewDelNode(serviceId, nodeID string) (IQ, error) {
|
||||
func NewDelNode(serviceId, nodeID string) (*IQ, error) {
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return IQ{}, errors.New("cannot delete a node without a target node ID")
|
||||
return nil, errors.New("cannot delete a node without a target node ID")
|
||||
}
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &DeleteOwner{Node: nodeID},
|
||||
}
|
||||
@@ -135,8 +147,11 @@ func NewDelNode(serviceId, nodeID string) (IQ, error) {
|
||||
|
||||
// NewPurgeAllItems creates a new purge request for the "nodeId" node, at "serviceId" service
|
||||
// See 8.5 Purge All Node Items
|
||||
func NewPurgeAllItems(serviceId, nodeId string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
func NewPurgeAllItems(serviceId, nodeId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &PurgeOwner{Node: nodeId},
|
||||
}
|
||||
@@ -145,8 +160,11 @@ func NewPurgeAllItems(serviceId, nodeId string) (IQ, error) {
|
||||
|
||||
// NewRequestDefaultConfig build a request to ask the service for the default config of its nodes
|
||||
// See 8.3 Request Default Node Configuration Options
|
||||
func NewRequestDefaultConfig(serviceId string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewRequestDefaultConfig(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &DefaultOwner{},
|
||||
}
|
||||
@@ -177,8 +195,11 @@ func NewApproveSubRequest(serviceId, reqID string, apprForm *Form) (Message, err
|
||||
// NewGetPendingSubRequests creates a new request for all pending subscriptions to all their nodes at a service
|
||||
// This feature MUST be implemented using the Ad-Hoc Commands (XEP-0050) protocol
|
||||
// 8.7 Process Pending Subscription Requests
|
||||
func NewGetPendingSubRequests(serviceId string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
func NewGetPendingSubRequests(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &Command{
|
||||
// the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending"
|
||||
Node: "http://jabber.org/protocol/pubsub#get-pending",
|
||||
@@ -191,23 +212,29 @@ func NewGetPendingSubRequests(serviceId string) (IQ, error) {
|
||||
// Upon receiving the data form for managing subscription requests, the owner then MAY request pending subscription
|
||||
// approval requests for a given node.
|
||||
// See 8.7.4 Per-Node Request
|
||||
func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error) {
|
||||
func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (*IQ, error) {
|
||||
if sessionId == "" {
|
||||
return IQ{}, errors.New("the sessionId must be maintained for the command")
|
||||
return nil, errors.New("the sessionId must be maintained for the command")
|
||||
}
|
||||
|
||||
form := &Form{
|
||||
Type: FormTypeSubmit,
|
||||
Fields: []Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}},
|
||||
Fields: []*Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}},
|
||||
}
|
||||
data, err := xml.Marshal(form)
|
||||
if err != nil {
|
||||
return IQ{}, err
|
||||
return nil, err
|
||||
}
|
||||
var n Node
|
||||
xml.Unmarshal(data, &n)
|
||||
err = xml.Unmarshal(data, &n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &Command{
|
||||
// the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending"
|
||||
Node: "http://jabber.org/protocol/pubsub#get-pending",
|
||||
@@ -221,16 +248,22 @@ func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error
|
||||
// NewSubListRequest creates a request to list subscriptions of the client, for all nodes at the service.
|
||||
// It's a Get type IQ
|
||||
// 8.8.1 Retrieve Subscriptions
|
||||
func NewSubListRqPl(serviceId, nodeID string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewSubListRqPl(serviceId, nodeID string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &SubscriptionsOwner{Node: nodeID},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &SubscriptionsOwner{Node: nodeID, Subscriptions: subs},
|
||||
}
|
||||
@@ -239,8 +272,11 @@ func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwne
|
||||
|
||||
// NewModifAffiliationRequest creates a request to either modify one or more affiliations, or delete one or more affiliations
|
||||
// 8.9.2 Modify Affiliation & 8.9.2.4 Multiple Simultaneous Modifications & 8.9.3 Delete an Entity (just set the status to "none")
|
||||
func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &AffiliationsOwner{
|
||||
Node: nodeID,
|
||||
@@ -252,8 +288,11 @@ func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []Affiliatio
|
||||
|
||||
// NewAffiliationListRequest creates a request to list all affiliated entities
|
||||
// See 8.9.1 Retrieve List List
|
||||
func NewAffiliationListRequest(serviceId, nodeID string) (IQ, error) {
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
func NewAffiliationListRequest(serviceId, nodeID string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &AffiliationsOwner{
|
||||
Node: nodeID,
|
||||
@@ -262,25 +301,47 @@ func NewAffiliationListRequest(serviceId, nodeID string) (IQ, error) {
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewFormSubmission builds a form submission pubsub IQ, in the Owner namespace
|
||||
// This is typically used to respond to a form issued by the server when configuring a node.
|
||||
// See 8.2.4 Form Submission
|
||||
func NewFormSubmissionOwner(serviceId, nodeName string, fields []*Field) (*IQ, error) {
|
||||
if serviceId == "" || nodeName == "" {
|
||||
return nil, errors.New("serviceId and nodeName must be filled for this request to be valid")
|
||||
}
|
||||
|
||||
submitConf, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitConf.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &ConfigureOwner{
|
||||
Node: nodeName,
|
||||
Form: NewForm(fields,
|
||||
FormTypeSubmit)},
|
||||
}
|
||||
|
||||
return submitConf, nil
|
||||
}
|
||||
|
||||
// GetFormFields gets the fields from a form in a IQ stanza of type result, as a map.
|
||||
// Key is the "var" attribute of the field, and field is the value.
|
||||
// The user can then select and modify the fields they want to alter, and submit a new form to the service using the
|
||||
// NewFormSubmission function to build the IQ.
|
||||
// TODO : remove restriction on IQ type ?
|
||||
func (iq *IQ) GetFormFields() (map[string]Field, error) {
|
||||
func (iq *IQ) GetFormFields() (map[string]*Field, error) {
|
||||
if iq.Type != IQTypeResult {
|
||||
return nil, errors.New("this IQ is not a result type IQ. Cannot extract the form from it")
|
||||
}
|
||||
switch payload := iq.Payload.(type) {
|
||||
// We support IOT Control IQ
|
||||
case *PubSubGeneric:
|
||||
fieldMap := make(map[string]Field)
|
||||
fieldMap := make(map[string]*Field)
|
||||
for _, elt := range payload.Configure.Form.Fields {
|
||||
fieldMap[elt.Var] = elt
|
||||
}
|
||||
return fieldMap, nil
|
||||
case *PubSubOwner:
|
||||
fieldMap := make(map[string]Field)
|
||||
fieldMap := make(map[string]*Field)
|
||||
co, ok := payload.OwnerUseCase.(*ConfigureOwner)
|
||||
if !ok {
|
||||
return nil, errors.New("this IQ does not contain a PubSub payload with a configure tag for the owner namespace")
|
||||
@@ -289,9 +350,20 @@ func (iq *IQ) GetFormFields() (map[string]Field, error) {
|
||||
fieldMap[elt.Var] = elt
|
||||
}
|
||||
return fieldMap, nil
|
||||
|
||||
case *Command:
|
||||
fieldMap := make(map[string]*Field)
|
||||
co, ok := payload.CommandElement.(*Form)
|
||||
if !ok {
|
||||
return nil, errors.New("this IQ does not contain a command payload with a form")
|
||||
}
|
||||
for _, elt := range co.Fields {
|
||||
fieldMap[elt.Var] = elt
|
||||
}
|
||||
return fieldMap, nil
|
||||
default:
|
||||
if iq.Any != nil {
|
||||
fieldMap := make(map[string]Field)
|
||||
fieldMap := make(map[string]*Field)
|
||||
if iq.Any.XMLName.Local != "command" {
|
||||
return nil, errors.New("this IQ does not contain a form")
|
||||
}
|
||||
@@ -307,7 +379,7 @@ func (iq *IQ) GetFormFields() (map[string]Field, error) {
|
||||
}
|
||||
err = xml.Unmarshal(data, &f)
|
||||
if err == nil {
|
||||
fieldMap[f.Var] = f
|
||||
fieldMap[f.Var] = &f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,33 +409,35 @@ func (pso *PubSubOwner) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err
|
||||
|
||||
case "affiliations":
|
||||
aff := AffiliationsOwner{}
|
||||
d.DecodeElement(&aff, &tt)
|
||||
err = d.DecodeElement(&aff, &tt)
|
||||
pso.OwnerUseCase = &aff
|
||||
case "configure":
|
||||
co := ConfigureOwner{}
|
||||
d.DecodeElement(&co, &tt)
|
||||
err = d.DecodeElement(&co, &tt)
|
||||
pso.OwnerUseCase = &co
|
||||
case "default":
|
||||
def := DefaultOwner{}
|
||||
d.DecodeElement(&def, &tt)
|
||||
err = d.DecodeElement(&def, &tt)
|
||||
pso.OwnerUseCase = &def
|
||||
case "delete":
|
||||
del := DeleteOwner{}
|
||||
d.DecodeElement(&del, &tt)
|
||||
err = d.DecodeElement(&del, &tt)
|
||||
pso.OwnerUseCase = &del
|
||||
case "purge":
|
||||
pu := PurgeOwner{}
|
||||
d.DecodeElement(&pu, &tt)
|
||||
err = d.DecodeElement(&pu, &tt)
|
||||
pso.OwnerUseCase = &pu
|
||||
case "subscriptions":
|
||||
subs := SubscriptionsOwner{}
|
||||
d.DecodeElement(&subs, &tt)
|
||||
err = d.DecodeElement(&subs, &tt)
|
||||
pso.OwnerUseCase = &subs
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
|
||||
@@ -16,10 +16,10 @@ func TestNewConfigureNode(t *testing.T) {
|
||||
"</pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
subR.Id = "config1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a configure node request: %v", err)
|
||||
}
|
||||
subR.Id = "config1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -129,10 +129,10 @@ func TestNewRequestDefaultConfig(t *testing.T) {
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <default></default> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit")
|
||||
subR.Id = "def1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a default config request: %v", err)
|
||||
}
|
||||
subR.Id = "def1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -239,10 +239,10 @@ func TestNewDelNode(t *testing.T) {
|
||||
"<delete node=\"princely_musings\"></delete> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
subR.Id = "delete1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a node delete request: %v", err)
|
||||
}
|
||||
subR.Id = "delete1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -311,10 +311,10 @@ func TestNewPurgeAllItems(t *testing.T) {
|
||||
"<purge node=\"princely_musings\"></purge> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings")
|
||||
subR.Id = "purge1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a purge all items request: %v", err)
|
||||
}
|
||||
subR.Id = "purge1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -357,7 +357,7 @@ func TestNewApproveSubRequest(t *testing.T) {
|
||||
|
||||
apprForm := &stanza.Form{
|
||||
Type: stanza.FormTypeSubmit,
|
||||
Fields: []stanza.Field{
|
||||
Fields: []*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#subscribe_authorization"}},
|
||||
{Var: "pubsub#subid", ValuesList: []string{"123-abc"}},
|
||||
{Var: "pubsub#node", ValuesList: []string{"princely_musings"}},
|
||||
@@ -367,10 +367,10 @@ func TestNewApproveSubRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm)
|
||||
subR.Id = "approve1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a sub approval request: %v", err)
|
||||
}
|
||||
subR.Id = "approve1"
|
||||
|
||||
frm, ok := subR.Extensions[0].(*stanza.Form)
|
||||
if !ok {
|
||||
@@ -381,7 +381,7 @@ func TestNewApproveSubRequest(t *testing.T) {
|
||||
|
||||
for _, f := range frm.Fields {
|
||||
if f.Var == "pubsub#allow" {
|
||||
allowField = &f
|
||||
allowField = f
|
||||
}
|
||||
}
|
||||
if allowField == nil || allowField.ValuesList[0] != "true" {
|
||||
@@ -404,10 +404,10 @@ func TestNewGetPendingSubRequests(t *testing.T) {
|
||||
"</command> </iq>"
|
||||
|
||||
subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit")
|
||||
subR.Id = "pending1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a get pending subs request: %v", err)
|
||||
}
|
||||
subR.Id = "pending1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -461,12 +461,12 @@ func TestNewGetPendingSubRequestsResp(t *testing.T) {
|
||||
|
||||
_, ok := respIQ.Payload.(*stanza.Command)
|
||||
if !ok {
|
||||
errors.New("this iq payload is not a command")
|
||||
t.Fatal("this iq payload is not a command")
|
||||
}
|
||||
|
||||
fMap, err := respIQ.GetFormFields()
|
||||
if err != nil || len(fMap) != 2 {
|
||||
errors.New("could not parse command form fields")
|
||||
t.Fatal("could not parse command form fields")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -485,10 +485,10 @@ func TestNewApprovePendingSubRequest(t *testing.T) {
|
||||
subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit",
|
||||
"pubsub-get-pending:20031021T150901Z-600",
|
||||
"princely_musings")
|
||||
subR.Id = "pending2"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a approve pending sub request: %v", err)
|
||||
}
|
||||
subR.Id = "pending2"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -524,10 +524,10 @@ func TestNewSubListRqPl(t *testing.T) {
|
||||
"<subscriptions node=\"princely_musings\"></subscriptions> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings")
|
||||
subR.Id = "subman1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a sub list request: %v", err)
|
||||
}
|
||||
subR.Id = "subman1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -575,7 +575,7 @@ func TestNewSubListRqPlResp(t *testing.T) {
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
errors.New("this iq payload is not a command")
|
||||
t.Fatal("this iq payload is not a command")
|
||||
}
|
||||
|
||||
subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner)
|
||||
@@ -599,10 +599,10 @@ func TestNewAffiliationListRequest(t *testing.T) {
|
||||
"<affiliations node=\"princely_musings\"></affiliations> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings")
|
||||
subR.Id = "ent1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create an affiliations list request: %v", err)
|
||||
}
|
||||
subR.Id = "ent1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -648,7 +648,7 @@ func TestNewAffiliationListRequestResp(t *testing.T) {
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
errors.New("this iq payload is not a command")
|
||||
t.Fatal("this iq payload is not a command")
|
||||
}
|
||||
|
||||
affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
|
||||
@@ -690,10 +690,10 @@ func TestNewModifAffiliationRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils)
|
||||
subR.Id = "ent3"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create request : %s", err)
|
||||
t.Fatalf("failed to create a modif affiliation request: %v", err)
|
||||
}
|
||||
subR.Id = "ent3"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -816,6 +816,58 @@ func TestGetFormFieldsCmd(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestNewFormSubmissionOwner(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"config2\" to=\"pubsub.shakespeare.lit\">" +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <configure node=\"princely_musings\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\" > <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||
"<value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#item_expire\"> " +
|
||||
"<value>604800</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
|
||||
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value> " +
|
||||
"<value>courtiers</value> </field> </x> </configure> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewFormSubmissionOwner("pubsub.shakespeare.lit",
|
||||
"princely_musings",
|
||||
[]*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#item_expire", ValuesList: []string{"604800"}},
|
||||
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
|
||||
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a form submission request: %v", err)
|
||||
}
|
||||
subR.Id = "config2"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||
}
|
||||
|
||||
conf, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||
if !ok {
|
||||
t.Fatalf("pubsub does not contain a configure node !")
|
||||
}
|
||||
|
||||
if conf.Form == nil {
|
||||
t.Fatalf("the form is absent from the configuration submission !")
|
||||
}
|
||||
if len(conf.Form.Fields) != 4 {
|
||||
t.Fatalf("expected 4 fields, found %d", len(conf.Form.Fields))
|
||||
}
|
||||
if len(conf.Form.Fields[3].ValuesList) != 3 {
|
||||
t.Fatalf("expected 3 values in fourth field, found %d", len(conf.Form.Fields[3].ValuesList))
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) {
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
@@ -826,7 +878,7 @@ func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) {
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
errors.New("this iq payload is not a pubsub of the owner namespace")
|
||||
return nil, errors.New("this iq payload is not a pubsub of the owner namespace")
|
||||
}
|
||||
|
||||
return pubsub, nil
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var submitFormExample = stanza.NewForm([]stanza.Field{
|
||||
var submitFormExample = stanza.NewForm([]*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}},
|
||||
{Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}},
|
||||
@@ -40,10 +40,10 @@ func TestNewSubRequest(t *testing.T) {
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo)
|
||||
subR.Id = "sub1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a sub request : %s", err)
|
||||
t.Fatalf("failed to create a sub request: %v", err)
|
||||
}
|
||||
subR.Id = "sub1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -96,10 +96,10 @@ func TestNewUnSubRequest(t *testing.T) {
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo)
|
||||
subR.Id = "unsub1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a sub request : %s", err)
|
||||
t.Fatalf("failed to create an unsub request: %v", err)
|
||||
}
|
||||
subR.Id = "unsub1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -157,10 +157,10 @@ func TestNewSubOptsRq(t *testing.T) {
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo)
|
||||
subR.Id = "options1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a sub request : %s", err)
|
||||
t.Fatalf("failed to create a sub options request: %v", err)
|
||||
}
|
||||
subR.Id = "options1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -264,10 +264,10 @@ func TestNewFormSubmission(t *testing.T) {
|
||||
}
|
||||
|
||||
subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||
subR.Id = "options2"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a sub request : %s", err)
|
||||
t.Fatalf("failed to create a form submission request: %v", err)
|
||||
}
|
||||
subR.Id = "options2"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -313,10 +313,10 @@ func TestNewSubAndConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||
subR.Id = "sub1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a sub request : %s", err)
|
||||
t.Fatalf("failed to create a sub and config request: %v", err)
|
||||
}
|
||||
subR.Id = "sub1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -482,10 +482,10 @@ func TestNewSpecificItemRequest(t *testing.T) {
|
||||
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </items> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901")
|
||||
subR.Id = "items3"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create an items request : %s", err)
|
||||
t.Fatalf("failed to create a specific item request: %v", err)
|
||||
}
|
||||
subR.Id = "items3"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
@@ -638,10 +638,10 @@ func TestNewDelItemFromNode(t *testing.T) {
|
||||
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </retract> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil)
|
||||
subR.Id = "retract1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a del item request : %s", err)
|
||||
t.Fatalf("failed to create a delete item from node request: %v", err)
|
||||
}
|
||||
subR.Id = "retract1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
@@ -677,10 +677,10 @@ func TestNewCreateNode(t *testing.T) {
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
subR.Id = "create1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a create node request : %s", err)
|
||||
t.Fatalf("failed to create a create node request: %v", err)
|
||||
}
|
||||
subR.Id = "create1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
@@ -741,17 +741,18 @@ func TestNewCreateAndConfigNode(t *testing.T) {
|
||||
"princely_musings",
|
||||
&stanza.Form{
|
||||
Type: stanza.FormTypeSubmit,
|
||||
Fields: []stanza.Field{
|
||||
Fields: []*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#notify_retract", ValuesList: []string{"0"}},
|
||||
{Var: "pubsub#notify_sub", ValuesList: []string{"0"}},
|
||||
{Var: "pubsub#max_payload_size", ValuesList: []string{"1028"}},
|
||||
},
|
||||
})
|
||||
subR.Id = "create1"
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a create node request : %s", err)
|
||||
t.Fatalf("failed to create a create and config node request: %v", err)
|
||||
}
|
||||
subR.Id = "create1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
@@ -796,10 +797,10 @@ func TestNewRetrieveAllSubsRequest(t *testing.T) {
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscriptions></subscriptions> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit")
|
||||
subR.Id = "subscriptions1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create a create node request : %s", err)
|
||||
t.Fatalf("failed to create a get all subs request: %v", err)
|
||||
}
|
||||
subR.Id = "subscriptions1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
@@ -856,10 +857,10 @@ func TestNewRetrieveAllAffilsRequest(t *testing.T) {
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <affiliations></affiliations> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit")
|
||||
subR.Id = "affil1"
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create retreive all affiliations request : %s", err)
|
||||
t.Fatalf("failed to create a get all affiliations request: %v", err)
|
||||
}
|
||||
subR.Id = "affil1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated retreive all affiliations request : %s", e)
|
||||
@@ -914,7 +915,7 @@ func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) {
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
errors.New("this iq payload is not a pubsub")
|
||||
return nil, errors.New("this iq payload is not a pubsub")
|
||||
}
|
||||
|
||||
return pubsub, nil
|
||||
|
||||
29
stanza/results_sets.go
Normal file
29
stanza/results_sets.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Support for XEP-0059
|
||||
// See https://xmpp.org/extensions/xep-0059
|
||||
const (
|
||||
// Common but not only possible namespace for query blocks in a result set context
|
||||
NSQuerySet = "jabber:iq:search"
|
||||
)
|
||||
|
||||
type ResultSet struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
|
||||
After *string `xml:"after,omitempty"`
|
||||
Before *string `xml:"before,omitempty"`
|
||||
Count *int `xml:"count,omitempty"`
|
||||
First *First `xml:"first,omitempty"`
|
||||
Index *int `xml:"index,omitempty"`
|
||||
Last *string `xml:"last,omitempty"`
|
||||
Max *int `xml:"max,omitempty"`
|
||||
}
|
||||
|
||||
type First struct {
|
||||
XMLName xml.Name `xml:"first"`
|
||||
Content string
|
||||
Index *int `xml:"index,attr,omitempty"`
|
||||
}
|
||||
28
stanza/results_sets_test.go
Normal file
28
stanza/results_sets_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Limiting the number of items
|
||||
func TestNewResultSetReq(t *testing.T) {
|
||||
expectedRq := "<iq id=\"q29302\" type=\"set\"> <query xmlns=\"urn:xmpp:mam:2\"> " +
|
||||
"<x type=\"submit\" xmlns=\"jabber:x:data\"> <field type=\"hidden\" var=\"FORM_TYPE\"> " +
|
||||
"<value>urn:xmpp:mam:2</value> </field> <field var=\"start\"> <value>2010-08-07T00:00:00Z</value> </field> </x> " +
|
||||
"<set xmlns=\"http://jabber.org/protocol/rsm\"> <max>10</max> </set> </query> </iq>"
|
||||
|
||||
maxVal := 10
|
||||
rs := &stanza.ResultSet{
|
||||
Max: &maxVal,
|
||||
}
|
||||
|
||||
// TODO when Mam is implemented
|
||||
_ = expectedRq
|
||||
_ = rs
|
||||
}
|
||||
|
||||
func TestUnmarshalResultSeqReq(t *testing.T) {
|
||||
// TODO when Mam is implemented
|
||||
|
||||
}
|
||||
@@ -69,12 +69,18 @@ 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"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (b *Bind) Namespace() string {
|
||||
return b.XMLName.Space
|
||||
}
|
||||
|
||||
func (b *Bind) GetSet() *ResultSet {
|
||||
return b.ResultSet
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session (Obsolete)
|
||||
|
||||
@@ -87,17 +93,23 @@ func (b *Bind) Namespace() string {
|
||||
// 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
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
||||
Optional *struct{} // If element does exist, it mean we are not required to open session
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StreamSession) Namespace() string {
|
||||
return s.XMLName.Space
|
||||
}
|
||||
|
||||
func (s *StreamSession) GetSet() *ResultSet {
|
||||
return s.ResultSet
|
||||
}
|
||||
|
||||
func (s *StreamSession) IsOptional() bool {
|
||||
if s.XMLName.Local == "session" {
|
||||
return s.Optional
|
||||
return s.Optional != nil
|
||||
}
|
||||
// If session element is missing, then we should not use session
|
||||
return true
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// Check that we can detect optional session from advertised stream features
|
||||
func TestSessionFeatures(t *testing.T) {
|
||||
streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: true}}
|
||||
streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: &struct{}{}}}
|
||||
|
||||
data, err := xml.Marshal(streamFeatures)
|
||||
if err != nil {
|
||||
@@ -28,8 +28,11 @@ func TestSessionFeatures(t *testing.T) {
|
||||
|
||||
// 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}
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: &struct{}{}}
|
||||
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
|
||||
171
stanza/stanza_errors.go
Normal file
171
stanza/stanza_errors.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type StanzaErrorGroup interface {
|
||||
GroupErrorName() string
|
||||
}
|
||||
|
||||
type BadFormat struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-format"`
|
||||
}
|
||||
|
||||
func (e *BadFormat) GroupErrorName() string { return "bad-format" }
|
||||
|
||||
type BadNamespacePrefix struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-namespace-prefix"`
|
||||
}
|
||||
|
||||
func (e *BadNamespacePrefix) GroupErrorName() string { return "bad-namespace-prefix" }
|
||||
|
||||
type Conflict struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas conflict"`
|
||||
}
|
||||
|
||||
func (e *Conflict) GroupErrorName() string { return "conflict" }
|
||||
|
||||
type ConnectionTimeout struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas connection-timeout"`
|
||||
}
|
||||
|
||||
func (e *ConnectionTimeout) GroupErrorName() string { return "connection-timeout" }
|
||||
|
||||
type HostGone struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-gone"`
|
||||
}
|
||||
|
||||
func (e *HostGone) GroupErrorName() string { return "host-gone" }
|
||||
|
||||
type HostUnknown struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-unknown"`
|
||||
}
|
||||
|
||||
func (e *HostUnknown) GroupErrorName() string { return "host-unknown" }
|
||||
|
||||
type ImproperAddressing struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas improper-addressing"`
|
||||
}
|
||||
|
||||
func (e *ImproperAddressing) GroupErrorName() string { return "improper-addressing" }
|
||||
|
||||
type InternalServerError struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error"`
|
||||
}
|
||||
|
||||
func (e *InternalServerError) GroupErrorName() string { return "internal-server-error" }
|
||||
|
||||
type InvalidForm struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-from"`
|
||||
}
|
||||
|
||||
func (e *InvalidForm) GroupErrorName() string { return "invalid-from" }
|
||||
|
||||
type InvalidId struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-id"`
|
||||
}
|
||||
|
||||
func (e *InvalidId) GroupErrorName() string { return "invalid-id" }
|
||||
|
||||
type InvalidNamespace struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-namespace"`
|
||||
}
|
||||
|
||||
func (e *InvalidNamespace) GroupErrorName() string { return "invalid-namespace" }
|
||||
|
||||
type InvalidXML struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-xml"`
|
||||
}
|
||||
|
||||
func (e *InvalidXML) GroupErrorName() string { return "invalid-xml" }
|
||||
|
||||
type NotAuthorized struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-authorized"`
|
||||
}
|
||||
|
||||
func (e *NotAuthorized) GroupErrorName() string { return "not-authorized" }
|
||||
|
||||
type NotWellFormed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-well-formed"`
|
||||
}
|
||||
|
||||
func (e *NotWellFormed) GroupErrorName() string { return "not-well-formed" }
|
||||
|
||||
type PolicyViolation struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas policy-violation"`
|
||||
}
|
||||
|
||||
func (e *PolicyViolation) GroupErrorName() string { return "policy-violation" }
|
||||
|
||||
type RemoteConnectionFailed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas remote-connection-failed"`
|
||||
}
|
||||
|
||||
func (e *RemoteConnectionFailed) GroupErrorName() string { return "remote-connection-failed" }
|
||||
|
||||
type Reset struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas reset"`
|
||||
}
|
||||
|
||||
func (e *Reset) GroupErrorName() string { return "reset" }
|
||||
|
||||
type ResourceConstraint struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint"`
|
||||
}
|
||||
|
||||
func (e *ResourceConstraint) GroupErrorName() string { return "resource-constraint" }
|
||||
|
||||
type RestrictedXML struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas restricted-xml"`
|
||||
}
|
||||
|
||||
func (e *RestrictedXML) GroupErrorName() string { return "restricted-xml" }
|
||||
|
||||
type SeeOtherHost struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas see-other-host"`
|
||||
}
|
||||
|
||||
func (e *SeeOtherHost) GroupErrorName() string { return "see-other-host" }
|
||||
|
||||
type SystemShutdown struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas system-shutdown"`
|
||||
}
|
||||
|
||||
func (e *SystemShutdown) GroupErrorName() string { return "system-shutdown" }
|
||||
|
||||
type UndefinedCondition struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition"`
|
||||
}
|
||||
|
||||
func (e *UndefinedCondition) GroupErrorName() string { return "undefined-condition" }
|
||||
|
||||
type UnsupportedEncoding struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-encoding"`
|
||||
}
|
||||
|
||||
type UnexpectedRequest struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request"`
|
||||
}
|
||||
|
||||
func (e *UnexpectedRequest) GroupErrorName() string { return "unexpected-request" }
|
||||
|
||||
func (e *UnsupportedEncoding) GroupErrorName() string { return "unsupported-encoding" }
|
||||
|
||||
type UnsupportedStanzaType struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-stanza-type"`
|
||||
}
|
||||
|
||||
func (e *UnsupportedStanzaType) GroupErrorName() string { return "unsupported-stanza-type" }
|
||||
|
||||
type UnsupportedVersion struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-version"`
|
||||
}
|
||||
|
||||
func (e *UnsupportedVersion) GroupErrorName() string { return "unsupported-version" }
|
||||
|
||||
type XMLNotWellFormed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas xml-not-well-formed"`
|
||||
}
|
||||
|
||||
func (e *XMLNotWellFormed) GroupErrorName() string { return "xml-not-well-formed" }
|
||||
@@ -118,6 +118,10 @@ type streamManagement struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
||||
}
|
||||
|
||||
func (streamManagement) Name() string {
|
||||
return "streamManagement"
|
||||
}
|
||||
|
||||
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
||||
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
||||
return true
|
||||
|
||||
@@ -3,12 +3,19 @@ package stanza
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
NSStreamManagement = "urn:xmpp:sm:3"
|
||||
)
|
||||
|
||||
type SMEnable struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 enable"`
|
||||
Max *uint `xml:"max,attr,omitempty"`
|
||||
Resume *bool `xml:"resume,attr,omitempty"`
|
||||
}
|
||||
|
||||
// Enabled as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#enable
|
||||
type SMEnabled struct {
|
||||
@@ -23,6 +30,112 @@ func (SMEnabled) Name() string {
|
||||
return "Stream Management: enabled"
|
||||
}
|
||||
|
||||
type UnAckQueue struct {
|
||||
Uslice []*UnAckedStz
|
||||
sync.RWMutex
|
||||
}
|
||||
type UnAckedStz struct {
|
||||
Id int
|
||||
Stz string
|
||||
}
|
||||
|
||||
func NewUnAckQueue() *UnAckQueue {
|
||||
return &UnAckQueue{
|
||||
Uslice: make([]*UnAckedStz, 0, 10), // Capacity is 0 to comply with "Push" implementation (so that no reachable element is nil)
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UnAckedStz) QueueableName() string {
|
||||
return "Un-acknowledged stanza"
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) PeekN(n int) []Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
if len(uaq.Uslice) < n {
|
||||
n = len(uaq.Uslice)
|
||||
}
|
||||
|
||||
if len(uaq.Uslice) == 0 {
|
||||
return nil
|
||||
}
|
||||
var r []Queueable
|
||||
for i := 0; i < n; i++ {
|
||||
r = append(r, uaq.Uslice[i])
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// No guarantee regarding thread safety !
|
||||
func (uaq *UnAckQueue) Pop() Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
r := uaq.Peek()
|
||||
if r != nil {
|
||||
uaq.Uslice = uaq.Uslice[1:]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// No guarantee regarding thread safety !
|
||||
func (uaq *UnAckQueue) PopN(n int) []Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
r := uaq.PeekN(n)
|
||||
uaq.Uslice = uaq.Uslice[len(r):]
|
||||
return r
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) Peek() Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
if len(uaq.Uslice) == 0 {
|
||||
return nil
|
||||
}
|
||||
r := uaq.Uslice[0]
|
||||
return r
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) Push(s Queueable) error {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
pushIdx := 1
|
||||
if len(uaq.Uslice) != 0 {
|
||||
pushIdx = uaq.Uslice[len(uaq.Uslice)-1].Id + 1
|
||||
}
|
||||
|
||||
sStz, ok := s.(*UnAckedStz)
|
||||
if !ok {
|
||||
return errors.New("element in not compatible with this queue. expected an UnAckedStz")
|
||||
}
|
||||
|
||||
e := UnAckedStz{
|
||||
Id: pushIdx,
|
||||
Stz: sStz.Stz,
|
||||
}
|
||||
|
||||
uaq.Uslice = append(uaq.Uslice, &e)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) Empty() bool {
|
||||
if uaq == nil {
|
||||
return true
|
||||
}
|
||||
r := len(uaq.Uslice)
|
||||
return r == 0
|
||||
}
|
||||
|
||||
// Request as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||
type SMRequest struct {
|
||||
@@ -37,7 +150,7 @@ func (SMRequest) Name() string {
|
||||
// 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"`
|
||||
H uint `xml:"h,attr"`
|
||||
}
|
||||
|
||||
func (SMAnswer) Name() string {
|
||||
@@ -49,24 +162,175 @@ func (SMAnswer) Name() string {
|
||||
type SMResumed struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"`
|
||||
PrevId string `xml:"previd,attr,omitempty"`
|
||||
H uint `xml:"h,attr,omitempty"`
|
||||
H *uint `xml:"h,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (SMResumed) Name() string {
|
||||
return "Stream Management: resumed"
|
||||
}
|
||||
|
||||
// Resume as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||
type SMResume struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 resume"`
|
||||
PrevId string `xml:"previd,attr,omitempty"`
|
||||
H *uint `xml:"h,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (SMResume) Name() string {
|
||||
return "Stream Management: resume"
|
||||
}
|
||||
|
||||
// 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).
|
||||
H *uint `xml:"h,attr,omitempty"`
|
||||
|
||||
StreamErrorGroup StanzaErrorGroup
|
||||
}
|
||||
|
||||
func (SMFailed) Name() string {
|
||||
return "Stream Management: failed"
|
||||
}
|
||||
|
||||
func (smf *SMFailed) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
smf.XMLName = start.Name
|
||||
|
||||
// According to https://xmpp.org/rfcs/rfc3920.html#def we should have no attributes aside from the namespace
|
||||
// which we don't use internally
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
// Decode sub-elements
|
||||
var err error
|
||||
switch tt.Name.Local {
|
||||
case "bad-format":
|
||||
bf := BadFormat{}
|
||||
err = d.DecodeElement(&bf, &tt)
|
||||
smf.StreamErrorGroup = &bf
|
||||
case "bad-namespace-prefix":
|
||||
bnp := BadNamespacePrefix{}
|
||||
err = d.DecodeElement(&bnp, &tt)
|
||||
smf.StreamErrorGroup = &bnp
|
||||
case "conflict":
|
||||
c := Conflict{}
|
||||
err = d.DecodeElement(&c, &tt)
|
||||
smf.StreamErrorGroup = &c
|
||||
case "connection-timeout":
|
||||
ct := ConnectionTimeout{}
|
||||
err = d.DecodeElement(&ct, &tt)
|
||||
smf.StreamErrorGroup = &ct
|
||||
case "host-gone":
|
||||
hg := HostGone{}
|
||||
err = d.DecodeElement(&hg, &tt)
|
||||
smf.StreamErrorGroup = &hg
|
||||
case "host-unknown":
|
||||
hu := HostUnknown{}
|
||||
err = d.DecodeElement(&hu, &tt)
|
||||
smf.StreamErrorGroup = &hu
|
||||
case "improper-addressing":
|
||||
ia := ImproperAddressing{}
|
||||
err = d.DecodeElement(&ia, &tt)
|
||||
smf.StreamErrorGroup = &ia
|
||||
case "internal-server-error":
|
||||
ise := InternalServerError{}
|
||||
err = d.DecodeElement(&ise, &tt)
|
||||
smf.StreamErrorGroup = &ise
|
||||
case "invalid-from":
|
||||
ifrm := InvalidForm{}
|
||||
err = d.DecodeElement(&ifrm, &tt)
|
||||
smf.StreamErrorGroup = &ifrm
|
||||
case "invalid-id":
|
||||
id := InvalidId{}
|
||||
err = d.DecodeElement(&id, &tt)
|
||||
smf.StreamErrorGroup = &id
|
||||
case "invalid-namespace":
|
||||
ins := InvalidNamespace{}
|
||||
err = d.DecodeElement(&ins, &tt)
|
||||
smf.StreamErrorGroup = &ins
|
||||
case "invalid-xml":
|
||||
ix := InvalidXML{}
|
||||
err = d.DecodeElement(&ix, &tt)
|
||||
smf.StreamErrorGroup = &ix
|
||||
case "not-authorized":
|
||||
na := NotAuthorized{}
|
||||
err = d.DecodeElement(&na, &tt)
|
||||
smf.StreamErrorGroup = &na
|
||||
case "not-well-formed":
|
||||
nwf := NotWellFormed{}
|
||||
err = d.DecodeElement(&nwf, &tt)
|
||||
smf.StreamErrorGroup = &nwf
|
||||
case "policy-violation":
|
||||
pv := PolicyViolation{}
|
||||
err = d.DecodeElement(&pv, &tt)
|
||||
smf.StreamErrorGroup = &pv
|
||||
case "remote-connection-failed":
|
||||
rcf := RemoteConnectionFailed{}
|
||||
err = d.DecodeElement(&rcf, &tt)
|
||||
smf.StreamErrorGroup = &rcf
|
||||
case "resource-constraint":
|
||||
rc := ResourceConstraint{}
|
||||
err = d.DecodeElement(&rc, &tt)
|
||||
smf.StreamErrorGroup = &rc
|
||||
case "restricted-xml":
|
||||
rx := RestrictedXML{}
|
||||
err = d.DecodeElement(&rx, &tt)
|
||||
smf.StreamErrorGroup = &rx
|
||||
case "see-other-host":
|
||||
soh := SeeOtherHost{}
|
||||
err = d.DecodeElement(&soh, &tt)
|
||||
smf.StreamErrorGroup = &soh
|
||||
case "system-shutdown":
|
||||
ss := SystemShutdown{}
|
||||
err = d.DecodeElement(&ss, &tt)
|
||||
smf.StreamErrorGroup = &ss
|
||||
case "undefined-condition":
|
||||
uc := UndefinedCondition{}
|
||||
err = d.DecodeElement(&uc, &tt)
|
||||
smf.StreamErrorGroup = &uc
|
||||
case "unexpected-request":
|
||||
ur := UnexpectedRequest{}
|
||||
err = d.DecodeElement(&ur, &tt)
|
||||
smf.StreamErrorGroup = &ur
|
||||
case "unsupported-encoding":
|
||||
ue := UnsupportedEncoding{}
|
||||
err = d.DecodeElement(&ue, &tt)
|
||||
smf.StreamErrorGroup = &ue
|
||||
case "unsupported-stanza-type":
|
||||
ust := UnsupportedStanzaType{}
|
||||
err = d.DecodeElement(&ust, &tt)
|
||||
smf.StreamErrorGroup = &ust
|
||||
case "unsupported-version":
|
||||
uv := UnsupportedVersion{}
|
||||
err = d.DecodeElement(&uv, &tt)
|
||||
smf.StreamErrorGroup = &uv
|
||||
case "xml-not-well-formed":
|
||||
xnwf := XMLNotWellFormed{}
|
||||
err = d.DecodeElement(&xnwf, &tt)
|
||||
smf.StreamErrorGroup = &xnwf
|
||||
default:
|
||||
return errors.New("error is unknown")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type smDecoder struct{}
|
||||
|
||||
var sm smDecoder
|
||||
@@ -78,9 +342,11 @@ func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
return s.decodeEnabled(p, se)
|
||||
case "resumed":
|
||||
return s.decodeResumed(p, se)
|
||||
case "resume":
|
||||
return s.decodeResume(p, se)
|
||||
case "r":
|
||||
return s.decodeRequest(p, se)
|
||||
case "h":
|
||||
case "a":
|
||||
return s.decodeAnswer(p, se)
|
||||
case "failed":
|
||||
return s.decodeFailed(p, se)
|
||||
@@ -102,6 +368,11 @@ func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed,
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func (smDecoder) decodeResume(p *xml.Decoder, se xml.StartElement) (SMResume, error) {
|
||||
var packet SMResume
|
||||
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)
|
||||
|
||||
226
stanza/stream_management_test.go
Normal file
226
stanza/stream_management_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPopEmptyQueue(t *testing.T) {
|
||||
var uaq stanza.UnAckQueue
|
||||
popped := uaq.Pop()
|
||||
if popped != nil {
|
||||
t.Fatalf("queue is empty but something was popped !")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
toPush := stanza.UnAckedStz{
|
||||
Id: 3,
|
||||
Stz: `<iq type='submit'
|
||||
from='confucius@scholars.lit/home'
|
||||
to='registrar.scholars.lit'
|
||||
id='kj3b157n'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:register'>
|
||||
<username>confucius</username>
|
||||
<first>Qui</first>
|
||||
<last>Kong</last>
|
||||
</query>
|
||||
</iq>`,
|
||||
}
|
||||
|
||||
err := uaq.Push(&toPush)
|
||||
if err != nil {
|
||||
t.Fatalf("could not push element to the queue : %v", err)
|
||||
}
|
||||
|
||||
if len(uaq.Uslice) != 4 {
|
||||
t.Fatalf("push to the non-acked queue failed")
|
||||
}
|
||||
for i := 0; i < 4; i++ {
|
||||
if uaq.Uslice[i].Id != i+1 {
|
||||
t.Fatalf("indexes were not updated correctly. Expected %d got %d", i, uaq.Uslice[i].Id)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the queue is a fifo : popped element should not be the one we just pushed.
|
||||
popped := uaq.Pop()
|
||||
poppedElt, ok := popped.(*stanza.UnAckedStz)
|
||||
if !ok {
|
||||
t.Fatalf("popped element is not a *stanza.UnAckedStz")
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(*poppedElt, toPush) {
|
||||
t.Fatalf("pushed element is at the top of the fifo queue when it should be at the bottom")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeekUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
|
||||
expectedPeek := stanza.UnAckedStz{
|
||||
Id: 1,
|
||||
Stz: `<iq type='set'
|
||||
from='romeo@montague.net/home'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search2'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'>
|
||||
<last>Capulet</last>
|
||||
</query>
|
||||
</iq>`,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedPeek, *uaq.Uslice[0]) {
|
||||
t.Fatalf("peek failed to return the correct stanza")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeekNUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
randPop := rand.Int31n(int32(initLen))
|
||||
|
||||
peeked := uaq.PeekN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice) != initLen {
|
||||
t.Fatalf("queue length changed whith peek n operation : had %d found %d after peek", initLen, len(uaq.Uslice))
|
||||
}
|
||||
|
||||
if len(peeked) != int(randPop) {
|
||||
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", randPop, len(peeked))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeekNUnackTooLong(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
|
||||
// Have a random number of elements to peek that's greater than the queue size
|
||||
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
|
||||
|
||||
peeked := uaq.PeekN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice) != initLen {
|
||||
t.Fatalf("total length changed whith peek n operation : had %d found %d after pop", initLen, len(uaq.Uslice))
|
||||
}
|
||||
|
||||
if len(peeked) != initLen {
|
||||
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", initLen, len(peeked))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPopNUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
randPop := rand.Int31n(int32(initLen))
|
||||
|
||||
popped := uaq.PopN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice)+len(popped) != initLen {
|
||||
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
|
||||
}
|
||||
|
||||
for _, elt := range popped {
|
||||
for _, oldElt := range uaq.Uslice {
|
||||
if reflect.DeepEqual(elt, oldElt) {
|
||||
t.Fatalf("pop n operation duplicated some elements")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopNUnackTooLong(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
|
||||
// Have a random number of elements to pop that's greater than the queue size
|
||||
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
|
||||
|
||||
popped := uaq.PopN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice)+len(popped) != initLen {
|
||||
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
|
||||
}
|
||||
|
||||
for _, elt := range popped {
|
||||
for _, oldElt := range uaq.Uslice {
|
||||
if reflect.DeepEqual(elt, oldElt) {
|
||||
t.Fatalf("pop n operation duplicated some elements")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
|
||||
popped := uaq.Pop()
|
||||
|
||||
if len(uaq.Uslice)+1 != initLen {
|
||||
t.Fatalf("total length changed whith pop operation : had %d found %d after pop", initLen, len(uaq.Uslice)+1)
|
||||
}
|
||||
for _, oldElt := range uaq.Uslice {
|
||||
if reflect.DeepEqual(popped, oldElt) {
|
||||
t.Fatalf("pop n operation duplicated some elements")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func initUnAckQueue() stanza.UnAckQueue {
|
||||
q := []*stanza.UnAckedStz{
|
||||
{
|
||||
Id: 1,
|
||||
Stz: `<iq type='set'
|
||||
from='romeo@montague.net/home'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search2'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'>
|
||||
<last>Capulet</last>
|
||||
</query>
|
||||
</iq>`,
|
||||
},
|
||||
{Id: 2,
|
||||
Stz: `<iq type='get'
|
||||
from='juliet@capulet.com/balcony'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search3'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'/>
|
||||
</iq>`},
|
||||
{Id: 3,
|
||||
Stz: `<iq type='set'
|
||||
from='juliet@capulet.com/balcony'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search4'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'>
|
||||
<x xmlns='jabber:x:data' type='submit'>
|
||||
<field type='hidden' var='FORM_TYPE'>
|
||||
<value>jabber:iq:search</value>
|
||||
</field>
|
||||
<field var='x-gender'>
|
||||
<value>male</value>
|
||||
</field>
|
||||
</x>
|
||||
</query>
|
||||
</iq>`},
|
||||
}
|
||||
|
||||
return stanza.UnAckQueue{Uslice: q}
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
@@ -16,7 +16,7 @@ var reInsideWhtsp = regexp.MustCompile(`[\s\p{Zs}]`)
|
||||
// ============================================================================
|
||||
// Marshaller / unmarshaller test
|
||||
|
||||
func checkMarshalling(t *testing.T, iq stanza.IQ) (*stanza.IQ, error) {
|
||||
func checkMarshalling(t *testing.T, iq *stanza.IQ) (*stanza.IQ, error) {
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
|
||||
@@ -25,9 +25,9 @@ import (
|
||||
// set callback and trigger reconnection.
|
||||
type StreamClient interface {
|
||||
Connect() error
|
||||
Resume(state SMState) error
|
||||
Resume() error
|
||||
Send(packet stanza.Packet) error
|
||||
SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error)
|
||||
SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error)
|
||||
SendRaw(packet string) error
|
||||
Disconnect() error
|
||||
SetHandler(handler EventHandler)
|
||||
@@ -37,7 +37,7 @@ type StreamClient interface {
|
||||
// It is mostly use in callback to pass a limited subset of the stream client interface
|
||||
type Sender interface {
|
||||
Send(packet stanza.Packet) error
|
||||
SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error)
|
||||
SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error)
|
||||
SendRaw(packet string) error
|
||||
}
|
||||
|
||||
@@ -75,9 +75,7 @@ func (sm *StreamManager) Run() error {
|
||||
}
|
||||
|
||||
handler := func(e Event) error {
|
||||
switch e.State {
|
||||
case StateConnected:
|
||||
sm.Metrics.setConnectTime()
|
||||
switch e.State.state {
|
||||
case StateSessionEstablished:
|
||||
sm.Metrics.setLoginTime()
|
||||
case StateDisconnected:
|
||||
@@ -128,7 +126,7 @@ func (sm *StreamManager) resume(state SMState) error {
|
||||
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
||||
sm.Metrics = initMetrics()
|
||||
|
||||
if err = sm.client.Resume(state); err != nil {
|
||||
if err = sm.client.Resume(); err != nil {
|
||||
var actualErr ConnError
|
||||
if xerrors.As(err, &actualErr) {
|
||||
if actualErr.Permanent {
|
||||
@@ -152,11 +150,6 @@ func (sm *StreamManager) resume(state SMState) error {
|
||||
|
||||
type Metrics struct {
|
||||
startTime time.Time
|
||||
// ConnectTime returns the duration between client initiation of the TCP/IP
|
||||
// connection to the server and actual TCP/IP session establishment.
|
||||
// This time includes DNS resolution and can be slightly higher if the DNS
|
||||
// resolution result was not in cache.
|
||||
ConnectTime time.Duration
|
||||
// LoginTime returns the between client initiation of the TCP/IP
|
||||
// connection to the server and the return of the login result.
|
||||
// This includes ConnectTime, but also XMPP level protocol negotiation
|
||||
@@ -172,10 +165,6 @@ func initMetrics() *Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) setConnectTime() {
|
||||
m.ConnectTime = time.Since(m.startTime)
|
||||
}
|
||||
|
||||
func (m *Metrics) setLoginTime() {
|
||||
m.LoginTime = time.Since(m.startTime)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ const (
|
||||
testClientRawPort
|
||||
testClientIqPort
|
||||
testClientIqFailPort
|
||||
testClientPostConnectHook
|
||||
|
||||
// Client internal tests
|
||||
testClientStreamManagement
|
||||
)
|
||||
|
||||
// ClientHandler is passed by the test client to provide custom behaviour to
|
||||
@@ -130,13 +134,16 @@ func respondToIQ(t *testing.T, sc *ServerConn) {
|
||||
t.Fatalf("failed to receive IQ : %s", err.Error())
|
||||
}
|
||||
|
||||
if !iqReq.IsValid() {
|
||||
if vld, _ := iqReq.IsValid(); !vld {
|
||||
mockIQError(sc.connection)
|
||||
return
|
||||
}
|
||||
|
||||
// Crafting response
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"})
|
||||
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create iqResp: %v", err)
|
||||
}
|
||||
disco := iqResp.DiscoInfo()
|
||||
disco.AddFeatures("vcard-temp",
|
||||
`http://jabber.org/protocol/address`)
|
||||
@@ -156,12 +163,15 @@ func respondToIQ(t *testing.T, sc *ServerConn) {
|
||||
// When a presence stanza is automatically sent (right now it's the case in the client), we may want to discard it
|
||||
// and test further stanzas.
|
||||
func discardPresence(t *testing.T, sc *ServerConn) {
|
||||
sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deadline: %v", err)
|
||||
}
|
||||
defer sc.connection.SetDeadline(time.Time{})
|
||||
var presenceStz stanza.Presence
|
||||
|
||||
recvBuf := make([]byte, len(InitialPresence))
|
||||
_, err := sc.connection.Read(recvBuf[:]) // recv data
|
||||
_, err = sc.connection.Read(recvBuf[:]) // recv data
|
||||
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
@@ -170,7 +180,7 @@ func discardPresence(t *testing.T, sc *ServerConn) {
|
||||
t.Errorf("read error: %s", err)
|
||||
}
|
||||
}
|
||||
xml.Unmarshal(recvBuf, &presenceStz)
|
||||
err = xml.Unmarshal(recvBuf, &presenceStz)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected presence but this happened : %s", err.Error())
|
||||
@@ -179,10 +189,13 @@ func discardPresence(t *testing.T, sc *ServerConn) {
|
||||
|
||||
// Reads next request coming from the Component. Expecting it to be an IQ request
|
||||
func receiveIq(sc *ServerConn) (*stanza.IQ, error) {
|
||||
sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sc.connection.SetDeadline(time.Time{})
|
||||
var iqStz stanza.IQ
|
||||
err := sc.decoder.Decode(&iqStz)
|
||||
err = sc.decoder.Decode(&iqStz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrTransportProtocolNotSupported = errors.New("Transport protocol not supported")
|
||||
var ErrTLSNotSupported = errors.New("Transport does not support StartTLS")
|
||||
var ErrTransportProtocolNotSupported = errors.New("transport protocol not supported")
|
||||
var ErrTLSNotSupported = errors.New("transport does not support StartTLS")
|
||||
|
||||
// TODO: rename to transport config?
|
||||
type TransportConfiguration struct {
|
||||
@@ -67,7 +67,7 @@ func NewClientTransport(config TransportConfiguration) Transport {
|
||||
// will be returned.
|
||||
func NewComponentTransport(config TransportConfiguration) (Transport, error) {
|
||||
if strings.HasPrefix(config.Address, "ws:") || strings.HasPrefix(config.Address, "wss:") {
|
||||
return nil, fmt.Errorf("Components only support XMPP transport: %w", ErrTransportProtocolNotSupported)
|
||||
return nil, fmt.Errorf("components only support XMPP transport: %w", ErrTransportProtocolNotSupported)
|
||||
}
|
||||
|
||||
config.Address = ensurePort(config.Address, 5222)
|
||||
|
||||
@@ -24,7 +24,8 @@ type XMPPTransport struct {
|
||||
readWriter io.ReadWriter
|
||||
logFile io.Writer
|
||||
isSecure bool
|
||||
closeChan chan stanza.StreamClosePacket
|
||||
// Used to close TCP connection when a stream close message is received from the server
|
||||
closeChan chan stanza.StreamClosePacket
|
||||
}
|
||||
|
||||
var componentStreamOpen = fmt.Sprintf("<?xml version='1.0'?><stream:stream to='%%s' xmlns='%s' xmlns:stream='%s'>", stanza.NSComponent, stanza.NSStream)
|
||||
@@ -113,7 +114,7 @@ func (t *XMPPTransport) Ping() error {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return errors.New("Could not write ping")
|
||||
return errors.New("could not write ping")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user