26 Commits

Author SHA1 Message Date
remicorniere
1e92089f96 Merge pull request #155 from remicorniere/Stream_Management_Patch
Stream Management
2020-03-10 15:33:59 +00:00
rcorniere
7850d07d37 Renamed Hooks 2020-03-10 16:31:27 +01:00
rcorniere
477a2b114c Changelog and doc 2020-03-09 17:19:29 +01:00
rcorniere
7a932d0504 Added missing tests 2020-03-09 17:12:32 +01:00
rcorniere
eff622df76 Changelog update 2020-03-09 16:50:12 +01:00
rcorniere
5fcb1c4337 Refactor tests 2020-03-09 16:25:11 +01:00
rcorniere
e59a86c380 Refactor tests 2020-03-06 17:52:52 +01:00
rcorniere
0459144512 First commit with dirty tests 2020-03-06 16:44:01 +01:00
remicorniere
22ba8d1f4e Merge pull request #150 from jacksgt/patch-2
Remove "no depdencies" statement from README
2020-02-18 10:22:14 +00:00
remicorniere
fa59efbfdc Merge pull request #151 from FluuxIO/Rmv_Xtra_Deps
Removed unnecessary dependencies from the core lib go.mod
2020-02-18 10:13:58 +00:00
rcorniere
086ceb4047 Removed unnecessary dependencies from the core lib go.mod 2020-02-18 10:29:22 +01:00
Jack Henschel
35e3defc62 Remove "no depdencies" statement from README
As is apparent from the current go.mod file, this library definitely depends on various other libraries.
This in turn makes it depend on Go 1.13 (currently).
2020-02-11 16:29:52 +01:00
remicorniere
79cd7e37f1 Merge pull request #149 from remicorniere/Fixes
Various fixes
2020-01-31 14:25:25 +00:00
rcorniere
2083cbf29c Various fixes 2020-01-31 15:17:59 +01:00
remicorniere
928c1595ef Merge pull request #148 from remicorniere/ResultSetsRework
- Changed IQ stanzas to pointer semantics
- Fixed commands from v 0.4.0 and tests
- Added primitive Result Sets support (XEP-0059)
- Tests for Result sets are not implemented yet. Result sets seem to be fairly unused across servers and is a little weird to test without a specific implementing XEP like XEP-0313; because the implementations are different across XEPs. Therefore, as 313 is coming, I'll update the tests for XEP-0059 with it.
2020-01-31 11:18:36 +00:00
rcorniere
70ef1d575f Reset Tests
Will come with MaM (XEP-313) implementation
2020-01-31 12:06:53 +01:00
rcorniere
8798ff6fc1 - Changed IQ stanzas to pointer semantics
- Fixed commands from v 0.4.0 and tests
- Added primitive Result Sets support (XEP-0059)
2020-01-31 11:48:03 +01:00
remicorniere
3a3a15507e Update README.md 2020-01-20 12:24:01 +01:00
remicorniere
84665d8c13 Merge pull request #146 from remicorniere/PubSub_Example
Pub sub example update
2020-01-14 22:53:11 +00:00
CORNIERE Rémi
e9bda893d6 Added tests for new Owner namespace function 2020-01-14 23:47:18 +01:00
CORNIERE Rémi
1d1adb0c48 Example pubsub code cleanup 2020-01-14 23:13:13 +01:00
CORNIERE Rémi
20e02cc9ad Added node config 2020-01-14 22:47:49 +01:00
remicorniere
9b557a68b3 Merge pull request #145 from remicorniere/PubSub_Example
Added README.md to PubSub client example
2020-01-14 18:23:49 +00:00
CORNIERE Rémi
9ca9f48c89 Added README.md 2020-01-14 19:21:29 +01:00
remicorniere
6b0a036d07 Merge pull request #144 from remicorniere/PubSub_Example
PubSub example
2020-01-14 18:16:27 +00:00
CORNIERE Rémi
f3218c4afa PubSub example 2020-01-14 19:12:54 +01:00
71 changed files with 2967 additions and 460 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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{})
}

View File

@@ -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",

View File

@@ -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 => ./../

View File

@@ -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=

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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:

View File

@@ -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)
}

View 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
```

View 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")
}
}
}

View File

@@ -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
View File

@@ -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
View 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
View File

@@ -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

View File

@@ -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")
}
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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=

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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{})
}

View File

@@ -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)

View File

@@ -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{})
}

View File

@@ -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
View 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
}

View File

@@ -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,

View File

@@ -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"}},

View File

@@ -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{})
}

View File

@@ -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
}

View File

@@ -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{})
}

View File

@@ -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").

View File

@@ -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"`

View File

@@ -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 {

View File

@@ -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)
}
})
}

View File

@@ -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{})
}

View File

@@ -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)

View File

@@ -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{})
}

View File

@@ -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{})
}

View File

@@ -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{})
}

View File

@@ -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{})
}

View File

@@ -210,5 +210,4 @@ func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err
}
}
return nil
}

View File

@@ -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{})
}

View File

@@ -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),

View File

@@ -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{})
}

View File

@@ -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{},
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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"`
}

View 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
}

View File

@@ -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

View File

@@ -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
View 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" }

View File

@@ -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

View File

@@ -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)

View 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())
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}