Merge remote-tracking branch 'fluux/master'
This commit is contained in:
commit
f8c4ecb59d
38
.github/workflows/test.yaml
vendored
Normal file
38
.github/workflows/test.yaml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
name: Run tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- .github/workflows/test.yaml
|
||||||
|
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- .github/workflows/test.yaml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.13
|
||||||
|
uses: actions/setup-go@v1
|
||||||
|
with:
|
||||||
|
go-version: 1.13
|
||||||
|
id: go
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
go test ./... -v -race -coverprofile cover.out -covermode=atomic
|
||||||
|
- name: Convert coverage to lcov
|
||||||
|
uses: jandelgado/gcov2lcov-action@v1.0.0
|
||||||
|
with:
|
||||||
|
infile: cover.out
|
||||||
|
outfile: coverage.lcov
|
||||||
|
- name: Coveralls
|
||||||
|
uses: coverallsapp/github-action@v1.0.1
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.github_token }}
|
||||||
|
path-to-lcov: coverage.lcov
|
43
CHANGELOG.md
43
CHANGELOG.md
|
@ -1,5 +1,46 @@
|
||||||
# Fluux XMPP Changelog
|
# 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
|
||||||
|
|
||||||
|
- Added support for XEP-0060 (PubSub)
|
||||||
|
(no support for 6.5.4 Returning Some Items yet as it needs XEP-0059, Result Sets)
|
||||||
|
- Added support for XEP-0050 (Commands)
|
||||||
|
- Added support for XEP-0004 (Forms)
|
||||||
|
- Updated the client example with a TUI
|
||||||
|
- Make keepalive interval configurable #134
|
||||||
|
- Fix updating of EventManager.CurrentState #136
|
||||||
|
- Added callbacks for error management in Component and Client. Users must now provide a callback function when using NewClient/Component.
|
||||||
|
- Moved JID from xmpp package to stanza package
|
||||||
|
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Update requirements to go1.13
|
||||||
|
- Add a websocket transport
|
||||||
|
- Add Client.SendIQ method
|
||||||
|
- Add IQ result routes to the Router
|
||||||
|
- Fix SIGSEGV in xmpp_component (#126)
|
||||||
|
- Add tests for Component and code style fixes
|
||||||
|
|
||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
@ -14,4 +55,4 @@
|
||||||
|
|
||||||
### Code migration guide
|
### Code migration guide
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
FROM golang:1.13
|
|
||||||
WORKDIR /xmpp
|
|
||||||
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
|
|
||||||
COPY . ./
|
|
21
README.md
21
README.md
|
@ -1,6 +1,6 @@
|
||||||
# Fluux XMPP
|
# Fluux XMPP
|
||||||
|
|
||||||
[![Codeship Status for FluuxIO/xmpp](https://app.codeship.com/projects/dba7f300-d145-0135-6c51-26e28af241d2/status?branch=master)](https://app.codeship.com/projects/262399) [![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![codecov](https://codecov.io/gh/FluuxIO/go-xmpp/branch/master/graph/badge.svg)](https://codecov.io/gh/FluuxIO/go-xmpp)
|
[![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![Coverage Status](https://coveralls.io/repos/github/FluuxIO/go-xmpp/badge.svg?branch=master)](https://coveralls.io/github/FluuxIO/go-xmpp?branch=master)
|
||||||
|
|
||||||
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
||||||
|
|
||||||
|
@ -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 simple chatbot to control a service or a thing,
|
||||||
- For writing XMPP servers components.
|
- 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
|
## Configuration and connection
|
||||||
|
|
||||||
|
@ -52,6 +52,13 @@ config := xmpp.Config{
|
||||||
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
|
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
|
||||||
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
|
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
- [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
|
||||||
|
Note : "6.5.4 Returning Some Items" requires support for [XEP-0059: Result Set Management](https://xmpp.org/extensions/xep-0059.html),
|
||||||
|
and is therefore not supported yet.
|
||||||
|
- [XEP-0004: Data Forms](https://xmpp.org/extensions/xep-0004.html)
|
||||||
|
- [XEP-0050: Ad-Hoc Commands](https://xmpp.org/extensions/xep-0050.html)
|
||||||
|
|
||||||
## Package overview
|
## Package overview
|
||||||
|
|
||||||
### Stanza subpackage
|
### Stanza subpackage
|
||||||
|
@ -108,15 +115,16 @@ func main() {
|
||||||
Address: "localhost:5222",
|
Address: "localhost:5222",
|
||||||
},
|
},
|
||||||
Jid: "test@localhost",
|
Jid: "test@localhost",
|
||||||
Credential: xmpp.Password("Test"),
|
Credential: xmpp.Password("test"),
|
||||||
StreamLogger: os.Stdout,
|
StreamLogger: os.Stdout,
|
||||||
Insecure: true,
|
Insecure: true,
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
router.HandleFunc("message", handleMessage)
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%+v", err)
|
log.Fatalf("%+v", err)
|
||||||
}
|
}
|
||||||
|
@ -138,6 +146,11 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
_ = s.Send(reply)
|
_ = s.Send(reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reference documentation
|
## Reference documentation
|
||||||
|
|
|
@ -9,7 +9,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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"}
|
payload := CustomPayload{XMLName: xml.Name{Space: "my:custom:payload", Local: "query"}, Node: "test"}
|
||||||
iq.Payload = payload
|
iq.Payload = payload
|
||||||
|
|
||||||
|
@ -44,6 +47,9 @@ func (c CustomPayload) Namespace() string {
|
||||||
return c.XMLName.Space
|
return c.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func (c CustomPayload) GetSet() *stanza.ResultSet {
|
||||||
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
|
return nil
|
||||||
|
}
|
||||||
|
func init() {
|
||||||
|
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "my:custom:payload", Local: "query"}, CustomPayload{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,9 @@ func main() {
|
||||||
IQNamespaces("urn:xmpp:delegation:1").
|
IQNamespaces("urn:xmpp:delegation:1").
|
||||||
HandlerFunc(handleDelegation)
|
HandlerFunc(handleDelegation)
|
||||||
|
|
||||||
component, err := xmpp.NewComponent(opts, router)
|
component, err := xmpp.NewComponent(opts, router, func(err error) {
|
||||||
|
log.Println(err)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%+v", err)
|
log.Fatalf("%+v", err)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +80,7 @@ const (
|
||||||
// ctx.Opts
|
// ctx.Opts
|
||||||
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(*stanza.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -87,15 +89,18 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
return
|
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 {
|
switch info.Node {
|
||||||
case "":
|
case "":
|
||||||
discoInfoRoot(&iqResp, opts)
|
discoInfoRoot(iqResp, opts)
|
||||||
case pubsubNode:
|
case pubsubNode:
|
||||||
discoInfoPubSub(&iqResp)
|
discoInfoPubSub(iqResp)
|
||||||
case pepNode:
|
case pepNode:
|
||||||
discoInfoPEP(&iqResp)
|
discoInfoPEP(iqResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
|
@ -155,7 +160,7 @@ func discoInfoPEP(iqResp *stanza.IQ) {
|
||||||
|
|
||||||
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(*stanza.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -166,12 +171,12 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||||
}
|
}
|
||||||
forwardedPacket := delegation.Forwarded.Stanza
|
forwardedPacket := delegation.Forwarded.Stanza
|
||||||
fmt.Println(forwardedPacket)
|
fmt.Println(forwardedPacket)
|
||||||
forwardedIQ, ok := forwardedPacket.(stanza.IQ)
|
forwardedIQ, ok := forwardedPacket.(*stanza.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub)
|
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSubGeneric)
|
||||||
if !ok {
|
if !ok {
|
||||||
// We only support pubsub delegation
|
// We only support pubsub delegation
|
||||||
return
|
return
|
||||||
|
@ -179,8 +184,11 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
|
||||||
if pubsub.Publish.XMLName.Local == "publish" {
|
if pubsub.Publish.XMLName.Local == "publish" {
|
||||||
// Prepare pubsub IQ reply
|
// 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})
|
||||||
payload := stanza.PubSub{
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create iqResp: %v", err)
|
||||||
|
}
|
||||||
|
payload := stanza.PubSubGeneric{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "http://jabber.org/protocol/pubsub",
|
Space: "http://jabber.org/protocol/pubsub",
|
||||||
Local: "pubsub",
|
Local: "pubsub",
|
||||||
|
@ -188,7 +196,10 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||||
}
|
}
|
||||||
iqResp.Payload = &payload
|
iqResp.Payload = &payload
|
||||||
// Wrap the reply in delegation 'forward'
|
// 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{
|
delegPayload := stanza.Delegation{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "urn:xmpp:delegation:1",
|
Space: "urn:xmpp:delegation:1",
|
||||||
|
|
|
@ -5,7 +5,7 @@ go 1.13
|
||||||
require (
|
require (
|
||||||
github.com/processone/mpg123 v1.0.0
|
github.com/processone/mpg123 v1.0.0
|
||||||
github.com/processone/soundcloud 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 => ./../
|
replace gosrc.io/xmpp => ./../
|
||||||
|
|
109
_examples/go.sum
109
_examples/go.sum
|
@ -1,37 +1,82 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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.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-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
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-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/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/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=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
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=
|
||||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
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-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-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
@ -42,39 +87,89 @@ 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.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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
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/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/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/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/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/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=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
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/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/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/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/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
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/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=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-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/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=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -85,21 +180,35 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/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/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 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/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/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
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/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
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 v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
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=
|
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=
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
|
|
3
_examples/muc_bot/README.md
Normal file
3
_examples/muc_bot/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# XMPP Multi-User (MUC) chat bot example
|
||||||
|
|
||||||
|
This code shows how to build a simple basic XMPP Multi-User chat bot using Fluux Go XMPP library.
|
51
_examples/xmpp_chat_client/README.md
Normal file
51
_examples/xmpp_chat_client/README.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Chat TUI example
|
||||||
|
This is a simple chat example, with a TUI.
|
||||||
|
It shows the library usage and a few of its capabilities.
|
||||||
|
## How to run
|
||||||
|
### Build
|
||||||
|
You can build the client using :
|
||||||
|
```
|
||||||
|
go build -o example_client
|
||||||
|
```
|
||||||
|
and then run with (on unix for example):
|
||||||
|
```
|
||||||
|
./example_client
|
||||||
|
```
|
||||||
|
or you can simply build + run in one command while at the example directory root, like this:
|
||||||
|
```
|
||||||
|
go run xmpp_chat_client.go interface.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
The example needs a configuration file to run. A sample file is provided.
|
||||||
|
By default, the example will look for a file named "config" in the current directory.
|
||||||
|
To provide a different configuration file, pass the following argument to the example :
|
||||||
|
```
|
||||||
|
go run xmpp_chat_client.go interface.go -c /path/to/config
|
||||||
|
```
|
||||||
|
where /path/to/config is the path to the directory containing the configuration file. The configuration file must be named
|
||||||
|
"config" and be using the yaml format.
|
||||||
|
|
||||||
|
Required fields are :
|
||||||
|
```yaml
|
||||||
|
Server :
|
||||||
|
- full_address: "localhost:5222"
|
||||||
|
Client : # This is you
|
||||||
|
- jid: "testuser2@localhost"
|
||||||
|
- pass: "pass123" #Password in a config file yay
|
||||||
|
|
||||||
|
# Contacts list, ";" separated
|
||||||
|
Contacts : "testuser1@localhost;testuser3@localhost"
|
||||||
|
# Should we log stanzas ?
|
||||||
|
LogStanzas:
|
||||||
|
- logger_on: "true"
|
||||||
|
- logfile_path: "./logs" # Path to directory, not file.
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
Shortcuts :
|
||||||
|
- ctrl+space : switch between input window and menu window.
|
||||||
|
- While in input window :
|
||||||
|
- enter : sends a message if in message mode (see menu options)
|
||||||
|
- ctrl+e : sends a raw stanza when in raw mode (see menu options)
|
||||||
|
- ctrl+c : quit
|
13
_examples/xmpp_chat_client/config.yml
Normal file
13
_examples/xmpp_chat_client/config.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Sample config for the client
|
||||||
|
Server :
|
||||||
|
- full_address: "localhost:5222"
|
||||||
|
Client :
|
||||||
|
- jid: "testuser2@localhost"
|
||||||
|
- pass: "pass123" #Password in a config file yay
|
||||||
|
|
||||||
|
Contacts : "testuser1@localhost;testuser3@localhost"
|
||||||
|
|
||||||
|
LogStanzas:
|
||||||
|
- logger_on: "true"
|
||||||
|
- logfile_path: "./logs"
|
||||||
|
|
10
_examples/xmpp_chat_client/go.mod
Normal file
10
_examples/xmpp_chat_client/go.mod
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module go-xmpp/_examples/xmpp_chat_client
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
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.4.0
|
||||||
|
)
|
371
_examples/xmpp_chat_client/interface.go
Normal file
371
_examples/xmpp_chat_client/interface.go
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/awesome-gocui/gocui"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Windows
|
||||||
|
chatLogWindow = "clw" // Where (received and sent) messages are logged
|
||||||
|
chatInputWindow = "iw" // Where messages are written
|
||||||
|
rawInputWindow = "rw" // Where raw stanzas are written
|
||||||
|
contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable
|
||||||
|
menuWindow = "mw" // Where the menu is shown
|
||||||
|
disconnectMsg = "msg"
|
||||||
|
|
||||||
|
// Windows titles
|
||||||
|
chatLogWindowTitle = "Chat log"
|
||||||
|
menuWindowTitle = "Menu"
|
||||||
|
chatInputWindowTitle = "Write a message :"
|
||||||
|
rawInputWindowTitle = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :"
|
||||||
|
contactsListWindowTitle = "Contacts"
|
||||||
|
|
||||||
|
// Menu options
|
||||||
|
disconnect = "Disconnect"
|
||||||
|
askServerForRoster = "Ask server for roster"
|
||||||
|
rawMode = "Switch to Send Raw Mode"
|
||||||
|
messageMode = "Switch to Send Message Mode"
|
||||||
|
contactList = "Contacts list"
|
||||||
|
backFromContacts = "<- Go back"
|
||||||
|
)
|
||||||
|
|
||||||
|
// To store names of views on top
|
||||||
|
type viewsState struct {
|
||||||
|
input string // Which input view is on top
|
||||||
|
side string // Which side view is on top
|
||||||
|
contacts []string // Contacts list
|
||||||
|
currentContact string // Contact we are currently messaging
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Which window is on top currently on top of the other.
|
||||||
|
// This is the init setup
|
||||||
|
viewState = viewsState{
|
||||||
|
input: chatInputWindow,
|
||||||
|
side: menuWindow,
|
||||||
|
}
|
||||||
|
menuOptions = []string{contactList, rawMode, askServerForRoster, disconnect}
|
||||||
|
// Errors
|
||||||
|
servConnFail = errors.New("failed to connect to server. Check your configuration ? Exiting")
|
||||||
|
)
|
||||||
|
|
||||||
|
func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
|
||||||
|
if _, err := g.SetCurrentView(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return g.SetViewOnTop(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func layout(g *gocui.Gui) error {
|
||||||
|
maxX, maxY := g.Size()
|
||||||
|
|
||||||
|
if v, err := g.SetView(chatLogWindow, maxX/5, 0, maxX-1, 5*maxY/6-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = chatLogWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
v.Autoscroll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(contactsListWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = contactsListWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
// If we set this to true, the contacts list will "fit" in the window but if the number
|
||||||
|
// of contacts exceeds the maximum height, some contacts will be hidden...
|
||||||
|
// If set to false, we can scroll up and down the contact list... infinitely. Meaning lower lines
|
||||||
|
// will be unlimited and empty... Didn't find a way to quickfix yet.
|
||||||
|
v.Autoscroll = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = menuWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
v.Autoscroll = true
|
||||||
|
fmt.Fprint(v, strings.Join(menuOptions, "\n"))
|
||||||
|
if _, err = setCurrentViewOnTop(g, menuWindow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(rawInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = rawInputWindowTitle
|
||||||
|
v.Editable = true
|
||||||
|
v.Wrap = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(chatInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = chatInputWindowTitle
|
||||||
|
v.Editable = true
|
||||||
|
v.Wrap = true
|
||||||
|
|
||||||
|
if _, err = setCurrentViewOnTop(g, chatInputWindow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gocui.ErrQuit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an input text from the user to the backend while also printing it in the chatlog window.
|
||||||
|
// KeyEnter is viewed as "\n" by gocui, so messages should only be one line, whereas raw sending has a different key
|
||||||
|
// binding and therefor should work with this too (for multiple lines stanzas)
|
||||||
|
func writeInput(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
chatLogWindow, _ := g.View(chatLogWindow)
|
||||||
|
|
||||||
|
input := strings.Join(v.ViewBufferLines(), "\n")
|
||||||
|
|
||||||
|
fmt.Fprintln(chatLogWindow, "Me : ", input)
|
||||||
|
if viewState.input == rawInputWindow {
|
||||||
|
rawTextChan <- input
|
||||||
|
} else {
|
||||||
|
textChan <- input
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setKeyBindings(g *gocui.Gui) {
|
||||||
|
// ==========================
|
||||||
|
// All views
|
||||||
|
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Chat input
|
||||||
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Raw input
|
||||||
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlE, gocui.ModNone, writeInput); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Menu
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Contacts list
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Disconnect message
|
||||||
|
if err := g.SetKeybinding(disconnectMsg, gocui.KeyEnter, gocui.ModNone, delMsg); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// General
|
||||||
|
// Used to handle menu selections and navigations
|
||||||
|
func getLine(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
var l string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, cy := v.Cursor()
|
||||||
|
if l, err = v.Line(cy); err != nil {
|
||||||
|
l = ""
|
||||||
|
}
|
||||||
|
if viewState.side == menuWindow {
|
||||||
|
if l == contactList {
|
||||||
|
cv, _ := g.View(contactsListWindow)
|
||||||
|
viewState.side = contactsListWindow
|
||||||
|
g.SetViewOnTop(contactsListWindow)
|
||||||
|
g.SetCurrentView(contactsListWindow)
|
||||||
|
if len(cv.ViewBufferLines()) == 0 {
|
||||||
|
printContactsToWindow(g, viewState.contacts)
|
||||||
|
}
|
||||||
|
} else if l == disconnect {
|
||||||
|
maxX, maxY := g.Size()
|
||||||
|
msg := "You disconnected from the server. Press enter to quit."
|
||||||
|
if v, err := g.SetView(disconnectMsg, maxX/2-30, maxY/2, maxX/2-29+len(msg), maxY/2+2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(v, msg)
|
||||||
|
if _, err := g.SetCurrentView(disconnectMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
killChan <- disconnectErr
|
||||||
|
} else if l == askServerForRoster {
|
||||||
|
chlw, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Asking server for contacts list...")
|
||||||
|
rosterChan <- struct{}{}
|
||||||
|
} else if l == rawMode {
|
||||||
|
mw, _ := g.View(menuWindow)
|
||||||
|
viewState.input = rawInputWindow
|
||||||
|
g.SetViewOnTop(rawInputWindow)
|
||||||
|
g.SetCurrentView(rawInputWindow)
|
||||||
|
menuOptions[1] = messageMode
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
||||||
|
message := "Now sending in raw stanza mode"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
} else if l == messageMode {
|
||||||
|
mw, _ := g.View(menuWindow)
|
||||||
|
viewState.input = chatInputWindow
|
||||||
|
g.SetViewOnTop(chatInputWindow)
|
||||||
|
g.SetCurrentView(chatInputWindow)
|
||||||
|
menuOptions[1] = rawMode
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
||||||
|
message := "Now sending in messages mode"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
}
|
||||||
|
} else if viewState.side == contactsListWindow {
|
||||||
|
if l == backFromContacts {
|
||||||
|
viewState.side = menuWindow
|
||||||
|
g.SetViewOnTop(menuWindow)
|
||||||
|
g.SetCurrentView(menuWindow)
|
||||||
|
} else if l == "" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// Updating the current correspondent, back-end side.
|
||||||
|
CorrespChan <- l
|
||||||
|
viewState.currentContact = l
|
||||||
|
// Showing the selected contact in contacts list
|
||||||
|
cl, _ := g.View(contactsListWindow)
|
||||||
|
cts := cl.ViewBufferLines()
|
||||||
|
cl.Clear()
|
||||||
|
printContactsToWindow(g, cts)
|
||||||
|
// Showing a message to the user, and switching back to input after the new contact is selected.
|
||||||
|
message := "Now sending messages to : " + l + " in a private conversation"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
g.SetCurrentView(chatInputWindow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printContactsToWindow(g *gocui.Gui, contactsList []string) {
|
||||||
|
cl, _ := g.View(contactsListWindow)
|
||||||
|
for _, c := range contactsList {
|
||||||
|
c = strings.ReplaceAll(c, " *", "")
|
||||||
|
if c == viewState.currentContact {
|
||||||
|
fmt.Fprintf(cl, c+" *\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(cl, c+"\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing view between input and "menu/contacts" when pressing the specific key.
|
||||||
|
func nextView(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v == nil || v.Name() == chatInputWindow || v.Name() == rawInputWindow {
|
||||||
|
_, err := g.SetCurrentView(viewState.side)
|
||||||
|
return err
|
||||||
|
} else if v.Name() == menuWindow || v.Name() == contactsListWindow {
|
||||||
|
_, err := g.SetCurrentView(viewState.input)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not be reached right now
|
||||||
|
_, err := g.SetCurrentView(chatInputWindow)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v != nil {
|
||||||
|
cx, cy := v.Cursor()
|
||||||
|
// Avoid going below the list of contacts. Although lines are stored in the view as a slice
|
||||||
|
// in the used lib. Therefor, if the number of lines is too big, the cursor will go past the last line since
|
||||||
|
// increasing slice capacity is done by doubling it. Last lines will be "nil" and reachable by the cursor
|
||||||
|
// in a dynamic context (such as contacts list)
|
||||||
|
cv := g.CurrentView()
|
||||||
|
h := cv.LinesHeight()
|
||||||
|
if cy+1 >= h {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Lower cursor
|
||||||
|
if err := v.SetCursor(cx, cy+1); err != nil {
|
||||||
|
ox, oy := v.Origin()
|
||||||
|
if err := v.SetOrigin(ox, oy+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v != nil {
|
||||||
|
ox, oy := v.Origin()
|
||||||
|
cx, cy := v.Cursor()
|
||||||
|
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
|
||||||
|
if err := v.SetOrigin(ox, oy-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func delMsg(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := g.DeleteView(disconnectMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
errChan <- gocui.ErrQuit // Quit the program
|
||||||
|
return nil
|
||||||
|
}
|
339
_examples/xmpp_chat_client/xmpp_chat_client.go
Normal file
339
_examples/xmpp_chat_client/xmpp_chat_client.go
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/awesome-gocui/gocui"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
infoFormat = "====== "
|
||||||
|
// Default configuration
|
||||||
|
defaultConfigFilePath = "./"
|
||||||
|
|
||||||
|
configFileName = "config"
|
||||||
|
configType = "yaml"
|
||||||
|
logStanzasOn = "logger_on"
|
||||||
|
logFilePath = "logfile_path"
|
||||||
|
// Keys in config
|
||||||
|
serverAddressKey = "full_address"
|
||||||
|
clientJid = "jid"
|
||||||
|
clientPass = "pass"
|
||||||
|
configContactSep = ";"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CorrespChan = make(chan string, 1)
|
||||||
|
textChan = make(chan string, 5)
|
||||||
|
rawTextChan = make(chan string, 5)
|
||||||
|
killChan = make(chan error, 1)
|
||||||
|
errChan = make(chan error)
|
||||||
|
rosterChan = make(chan struct{})
|
||||||
|
|
||||||
|
logger *log.Logger
|
||||||
|
disconnectErr = errors.New("disconnecting client")
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Server map[string]string `mapstructure:"server"`
|
||||||
|
Client map[string]string `mapstructure:"client"`
|
||||||
|
Contacts string `string:"contact"`
|
||||||
|
LogStanzas map[string]string `mapstructure:"logstanzas"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Parse the flag with the config directory path as argument
|
||||||
|
flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+
|
||||||
|
" file you want to use. Config file should be named \"config\" and be in YAML format..")
|
||||||
|
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Read configuration
|
||||||
|
c := readConfig()
|
||||||
|
|
||||||
|
//================================
|
||||||
|
// Setup logger
|
||||||
|
on, err := strconv.ParseBool(c.LogStanzas[logStanzasOn])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if on {
|
||||||
|
f, err := os.OpenFile(path.Join(c.LogStanzas[logFilePath], "logs.txt"), os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
logger = log.New(f, "", log.Lshortfile|log.Ldate|log.Ltime)
|
||||||
|
logger.SetOutput(f)
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Create TUI
|
||||||
|
g, err := gocui.NewGui(gocui.OutputNormal, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
defer g.Close()
|
||||||
|
g.Highlight = true
|
||||||
|
g.Cursor = true
|
||||||
|
g.SelFgColor = gocui.ColorGreen
|
||||||
|
g.SetManagerFunc(layout)
|
||||||
|
setKeyBindings(g)
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Run TUI
|
||||||
|
go func() {
|
||||||
|
errChan <- g.MainLoop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Start XMPP client
|
||||||
|
go startClient(g, c)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err == gocui.ErrQuit {
|
||||||
|
log.Println("Closing client.")
|
||||||
|
} else {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startClient(g *gocui.Gui, config *config) {
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client setup
|
||||||
|
clientCfg := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: config.Server[serverAddressKey],
|
||||||
|
},
|
||||||
|
Jid: config.Client[clientJid],
|
||||||
|
Credential: xmpp.Password(config.Client[clientPass]),
|
||||||
|
Insecure: true}
|
||||||
|
|
||||||
|
var client *xmpp.Client
|
||||||
|
var err error
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
|
||||||
|
handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(msg)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := g.View(chatLogWindow)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(v, "%sIgnoring packet: %T\n", infoFormat, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
if msg.Error.Code != 0 {
|
||||||
|
_, err := fmt.Fprintf(v, "Error from server : %s : %s \n", msg.Error.Reason, msg.XMLName.Space)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(strings.TrimSpace(msg.Body)) != 0 {
|
||||||
|
_, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HandleFunc("message", handlerWithGui)
|
||||||
|
if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil {
|
||||||
|
log.Panicln(fmt.Sprintf("Could not create a new client ! %s", err))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client connection
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
msg := fmt.Sprintf("%sXMPP connection failed: %s", infoFormat, err)
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
v, err := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintf(v, msg)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
fmt.Println("Failed to connect to server. Exiting...")
|
||||||
|
errChan <- servConnFail
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Start working
|
||||||
|
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
|
||||||
|
clw, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintf(clw, infoFormat+"Now sending messages to "+viewState.currentContact+" in a private conversation\n")
|
||||||
|
CorrespChan <- viewState.currentContact
|
||||||
|
startMessaging(client, config, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMessaging(client xmpp.Sender, config *config, g *gocui.Gui) {
|
||||||
|
var text string
|
||||||
|
var correspondent string
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-killChan:
|
||||||
|
if err == disconnectErr {
|
||||||
|
sc := client.(xmpp.StreamClient)
|
||||||
|
sc.Disconnect()
|
||||||
|
} else {
|
||||||
|
logger.Println(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case text = <-textChan:
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, Type: stanza.MessageTypeChat}, Body: text}
|
||||||
|
if logger != nil {
|
||||||
|
raw, _ := xml.Marshal(reply)
|
||||||
|
logger.Println(string(raw))
|
||||||
|
}
|
||||||
|
err := client.Send(reply)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("There was a problem sending the message : %v", reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case text = <-rawTextChan:
|
||||||
|
if logger != nil {
|
||||||
|
logger.Println(text)
|
||||||
|
}
|
||||||
|
err := client.SendRaw(text)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("There was a problem sending the message : %v", text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case crrsp := <-CorrespChan:
|
||||||
|
correspondent = crrsp
|
||||||
|
case <-rosterChan:
|
||||||
|
askForRoster(client, g, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only reads and parses the configuration
|
||||||
|
func readConfig() *config {
|
||||||
|
viper.SetConfigName(configFileName) // name of config file (without extension)
|
||||||
|
viper.BindPFlags(pflag.CommandLine)
|
||||||
|
viper.AddConfigPath(viper.GetString("c")) // path to look for the config file in
|
||||||
|
err := viper.ReadInConfig() // Find and read the config file
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
log.Fatalf("%s %s", err, "Please make sure you give a path to the directory of the config and not to the config itself.")
|
||||||
|
} else {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigType(configType)
|
||||||
|
var config config
|
||||||
|
err = viper.Unmarshal(&config)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("Unable to decode Config: %s \n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have contacts to message
|
||||||
|
if len(strings.TrimSpace(config.Contacts)) == 0 {
|
||||||
|
log.Panicln("You appear to have no contacts to message !")
|
||||||
|
}
|
||||||
|
// Check logging
|
||||||
|
config.LogStanzas[logFilePath] = path.Clean(config.LogStanzas[logFilePath])
|
||||||
|
on, err := strconv.ParseBool(config.LogStanzas[logStanzasOn])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if d, e := isDirectory(config.LogStanzas[logFilePath]); (e != nil || !d) && on {
|
||||||
|
log.Panicln("The log file path could not be found or is not a directory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an error occurs, this is used to kill the client
|
||||||
|
func errorHandler(err error) {
|
||||||
|
killChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the menu panel of the view with the current user's roster, by asking the server.
|
||||||
|
func askForRoster(client xmpp.Sender, g *gocui.Gui, config *config) {
|
||||||
|
// Craft a roster request
|
||||||
|
req := stanza.NewIQ(stanza.Attrs{From: config.Client[clientJid], Type: stanza.IQTypeGet})
|
||||||
|
req.RosterItems()
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(req)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
|
||||||
|
// Send the roster request to the server
|
||||||
|
c, err := client.SendIQ(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending a IQ has a channel spawned to process the response once we receive it.
|
||||||
|
// In order not to block the client, we spawn a goroutine to update the TUI once the server has responded.
|
||||||
|
go func() {
|
||||||
|
serverResp := <-c
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(serverResp)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
// Update contacts with the response from the server
|
||||||
|
chlw, _ := g.View(chatLogWindow)
|
||||||
|
if rosterItems, ok := serverResp.Payload.(*stanza.RosterItems); ok {
|
||||||
|
viewState.contacts = []string{}
|
||||||
|
for _, item := range rosterItems.Items {
|
||||||
|
viewState.contacts = append(viewState.contacts, item.Jid)
|
||||||
|
}
|
||||||
|
// Put a "go back" button at the end of the list
|
||||||
|
viewState.contacts = append(viewState.contacts, backFromContacts)
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Contacts list updated !")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Failed to update contact list !")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDirectory(path string) (bool, error) {
|
||||||
|
fileInfo, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return fileInfo.IsDir(), err
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ func main() {
|
||||||
IQNamespaces("jabber:iq:version").
|
IQNamespaces("jabber:iq:version").
|
||||||
HandlerFunc(handleVersion)
|
HandlerFunc(handleVersion)
|
||||||
|
|
||||||
component, err := xmpp.NewComponent(opts, router)
|
component, err := xmpp.NewComponent(opts, router, handleError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%+v", err)
|
log.Fatalf("%+v", err)
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,10 @@ func main() {
|
||||||
log.Fatal(cm.Run())
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleError(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||||
msg, ok := p.(stanza.Message)
|
msg, ok := p.(stanza.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -57,12 +61,16 @@ func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
|
||||||
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(*stanza.IQ)
|
||||||
if !ok || iq.Type != "get" {
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
return
|
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 := iqResp.DiscoInfo()
|
||||||
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
@ -72,8 +80,8 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
// TODO: Handle iq error responses
|
// TODO: Handle iq error responses
|
||||||
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(*stanza.IQ)
|
||||||
if !ok || iq.Type != "get" {
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +90,11 @@ func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||||
return
|
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()
|
items := iqResp.DiscoItems()
|
||||||
|
|
||||||
if discoItems.Node == "" {
|
if discoItems.Node == "" {
|
||||||
|
@ -93,12 +105,15 @@ func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||||
|
|
||||||
func handleVersion(c xmpp.Sender, p stanza.Packet) {
|
func handleVersion(c xmpp.Sender, p stanza.Packet) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(*stanza.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
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", "")
|
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|
4
_examples/xmpp_component2/README.md
Normal file
4
_examples/xmpp_component2/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# xmpp_component2
|
||||||
|
|
||||||
|
|
||||||
|
This program is an example of the simplest XMPP component: it connects to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response.
|
79
_examples/xmpp_component2/main.go
Normal file
79
_examples/xmpp_component2/main.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Connect to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
domain = "mycomponent.localhost"
|
||||||
|
address = "build.vpn.p1:8888"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init and return a component
|
||||||
|
func makeComponent() *xmpp.Component {
|
||||||
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: address,
|
||||||
|
Domain: domain,
|
||||||
|
},
|
||||||
|
Domain: domain,
|
||||||
|
Secret: "secret",
|
||||||
|
}
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
c, err := xmpp.NewComponent(opts, router, handleError)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := makeComponent()
|
||||||
|
|
||||||
|
// Connect Component to the server
|
||||||
|
fmt.Printf("Connecting to %v\n", address)
|
||||||
|
err := c.Connect()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a disco iq
|
||||||
|
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
|
||||||
|
|
||||||
|
// res is the channel used to receive the result iq
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
res, _ := c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case iqResponse := <-res:
|
||||||
|
// Got response from server
|
||||||
|
fmt.Print(iqResponse.Payload)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
panic("No iq response was received in time")
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ func main() {
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
router.HandleFunc("message", handleMessage)
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%+v", err)
|
log.Fatalf("%+v", err)
|
||||||
}
|
}
|
||||||
|
@ -50,3 +50,7 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
_ = s.Send(reply)
|
_ = s.Send(reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
37
_examples/xmpp_jukebox/README.md
Normal file
37
_examples/xmpp_jukebox/README.md
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Jukebox example
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- You need mpg123 installed on your computer because the example runs it as a command :
|
||||||
|
[Official MPG123 website](https://mpg123.de/)
|
||||||
|
Most linux distributions have a package for it.
|
||||||
|
- You need a soundcloud ID to play a music from the website through mpg123. You currently cannot play music files with this example.
|
||||||
|
Your user ID is available in your account settings on the [soundcloud website](https://soundcloud.com/)
|
||||||
|
**One is provided for convenience.**
|
||||||
|
- You need a running jabber server. You can run your local instance of [ejabberd](https://www.ejabberd.im/) for example.
|
||||||
|
- You need a registered user on the running jabber server.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
You can edit the soundcloud ID in the example file with your own, or use the provided one :
|
||||||
|
```go
|
||||||
|
const scClientID = "dde6a0075614ac4f3bea423863076b22"
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the example, build it with (while in the example directory) :
|
||||||
|
```
|
||||||
|
go build xmpp_jukebox.go
|
||||||
|
```
|
||||||
|
|
||||||
|
then run it with (update the command arguments accordingly):
|
||||||
|
```
|
||||||
|
./xmpp_jukebox -jid=MY_USERE@MY_DOMAIN/jukebox -password=MY_PASSWORD -address=MY_SERVER:MY_SERVER_PORT
|
||||||
|
```
|
||||||
|
Make sure to have a resource, for instance "/jukebox", on your jid.
|
||||||
|
|
||||||
|
Then you can send the following stanza to "MY_USERE@MY_DOMAIN/jukebox" (with the resource) to play a song (update the soundcloud URL accordingly) :
|
||||||
|
```xml
|
||||||
|
<iq id="1" to="MY_USERE@MY_DOMAIN/jukebox" type="set">
|
||||||
|
<set xml:lang="en" xmlns="urn:xmpp:iot:control">
|
||||||
|
<string name="url" value="https://soundcloud.com/UPDATE/ME"/>
|
||||||
|
</set>
|
||||||
|
</iq>
|
||||||
|
```
|
|
@ -3,6 +3,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
@ -19,7 +20,7 @@ import (
|
||||||
const scClientID = "dde6a0075614ac4f3bea423863076b22"
|
const scClientID = "dde6a0075614ac4f3bea423863076b22"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
jid := flag.String("jid", "", "jukebok XMPP JID, resource is optional")
|
jid := flag.String("jid", "", "jukebok XMPP Jid, resource is optional")
|
||||||
password := flag.String("password", "", "XMPP account password")
|
password := flag.String("password", "", "XMPP account password")
|
||||||
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
|
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
@ -48,12 +49,12 @@ func main() {
|
||||||
handleMessage(s, p, player)
|
handleMessage(s, p, player)
|
||||||
})
|
})
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
Packet("message").
|
Packet("iq").
|
||||||
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
handleIQ(s, p, player)
|
handleIQ(s, p, player)
|
||||||
})
|
})
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%+v", err)
|
log.Fatalf("%+v", err)
|
||||||
}
|
}
|
||||||
|
@ -61,6 +62,9 @@ func main() {
|
||||||
cm := xmpp.NewStreamManager(client, nil)
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
log.Fatal(cm.Run())
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
msg, ok := p.(stanza.Message)
|
msg, ok := p.(stanza.Message)
|
||||||
|
@ -77,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) {
|
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(*stanza.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -96,7 +100,7 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
setResponse := new(stanza.ControlSetResponse)
|
setResponse := new(stanza.ControlSetResponse)
|
||||||
// FIXME: Broken
|
// FIXME: Broken
|
||||||
reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
|
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
|
// TODO add Soundclound artist / title retrieval
|
||||||
sendUserTune(s, "Radiohead", "Spectre")
|
sendUserTune(s, "Radiohead", "Spectre")
|
||||||
default:
|
default:
|
||||||
|
@ -105,11 +109,29 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
||||||
tune := stanza.Tune{Artist: artist, Title: title}
|
rq, err := stanza.NewPublishItemRq("localhost",
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
|
"http://jabber.org/protocol/tune",
|
||||||
payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
|
"",
|
||||||
iq.Payload = &payload
|
stanza.Item{
|
||||||
_ = s.Send(iq)
|
XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"},
|
||||||
|
Any: &stanza.Node{
|
||||||
|
Nodes: []stanza.Node{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Local: "artist"},
|
||||||
|
Content: artist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Local: "title"},
|
||||||
|
Content: title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to build the publish request : %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.Send(rq)
|
||||||
}
|
}
|
||||||
|
|
||||||
func playSCURL(p *mpg123.Player, rawURL string) {
|
func playSCURL(p *mpg123.Player, rawURL string) {
|
||||||
|
@ -117,7 +139,7 @@ func playSCURL(p *mpg123.Player, rawURL string) {
|
||||||
// TODO: Maybe we need to check the track itself to get the stream URL from reply ?
|
// TODO: Maybe we need to check the track itself to get the stream URL from reply ?
|
||||||
url := soundcloud.FormatStreamURL(songID)
|
url := soundcloud.FormatStreamURL(songID)
|
||||||
|
|
||||||
_ = p.Play(url)
|
_ = p.Play(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
|
|
@ -28,7 +28,7 @@ func main() {
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
router.HandleFunc("message", handleMessage)
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%+v", err)
|
log.Fatalf("%+v", err)
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,10 @@ func main() {
|
||||||
log.Fatal(cm.Run())
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
msg, ok := p.(stanza.Message)
|
msg, ok := p.(stanza.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
17
_examples/xmpp_pubsub_client/README.md
Normal file
17
_examples/xmpp_pubsub_client/README.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# PubSub client example
|
||||||
|
|
||||||
|
## Description
|
||||||
|
This is a simple example of a client that :
|
||||||
|
* Creates a node on a service
|
||||||
|
* Subscribes to that node
|
||||||
|
* Publishes to that node
|
||||||
|
* Gets the notification from the publication and prints it on screen
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
You need to have a running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html).
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
Just run :
|
||||||
|
```
|
||||||
|
go run xmpp_ps_client.go
|
||||||
|
```
|
278
_examples/xmpp_pubsub_client/xmpp_ps_client.go
Normal file
278
_examples/xmpp_pubsub_client/xmpp_ps_client.go
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userJID = "testuser2@localhost"
|
||||||
|
serverAddress = "localhost:5222"
|
||||||
|
nodeName = "lel_node"
|
||||||
|
serviceName = "pubsub.localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
var invalidResp = errors.New("invalid response")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: serverAddress,
|
||||||
|
},
|
||||||
|
Jid: userJID,
|
||||||
|
Credential: xmpp.Password("pass123"),
|
||||||
|
// StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.NewRoute().Packet("message").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
data, _ := xml.Marshal(p)
|
||||||
|
log.Println("Received a message ! => \n" + string(data))
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) })
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client connection
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Create a node
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
createNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ================================================================================
|
||||||
|
// Configure the node. This can also be done in a single message with the creation
|
||||||
|
configureNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// Subscribe to this node :
|
||||||
|
subToNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Publish to that node
|
||||||
|
pubToNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Let's purge the node :
|
||||||
|
purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName)
|
||||||
|
purgeCh, err := client.SendIQ(ctx, purgeRq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not send purge request: %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case purgeResp := <-purgeCh:
|
||||||
|
|
||||||
|
if purgeResp.Type == stanza.IQTypeError {
|
||||||
|
cancel()
|
||||||
|
if vld, err := purgeResp.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", purgeResp, err)
|
||||||
|
}
|
||||||
|
log.Fatalf("error while purging node : %s", purgeResp.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println("node successfully purged")
|
||||||
|
case <-time.After(1000 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while purging node")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
rqCreate, err := stanza.NewCreateNode(serviceName, nodeName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
createCh, err := client.SendIQ(ctx, rqCreate)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if createCh != nil {
|
||||||
|
select {
|
||||||
|
case respCr := <-createCh:
|
||||||
|
// Got response from server
|
||||||
|
if respCr.Type == stanza.IQTypeError {
|
||||||
|
if vld, err := respCr.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %+v"+" reason: %s", respCr, err)
|
||||||
|
}
|
||||||
|
if respCr.Error.Reason != "conflict" {
|
||||||
|
log.Fatalf("%+v", respCr.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println(respCr.Error.Text)
|
||||||
|
} else {
|
||||||
|
fmt.Print("successfully created channel")
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while creating node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
// First, ask for a form with the config options
|
||||||
|
confRq, _ := stanza.NewConfigureNode(serviceName, nodeName)
|
||||||
|
confReqCh, err := client.SendIQ(ctx, confRq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not send iq : %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case confForm := <-confReqCh:
|
||||||
|
// If the request was successful, we now have a form with configuration options to update
|
||||||
|
fields, err := confForm.GetFormFields()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("No config fields found !")
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are some common fields expected to be present. Change processing to your liking
|
||||||
|
if fields["pubsub#max_payload_size"] != nil {
|
||||||
|
fields["pubsub#max_payload_size"].ValuesList[0] = "100000"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fields["pubsub#notification_type"] != nil {
|
||||||
|
fields["pubsub#notification_type"].ValuesList[0] = "headline"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the modified fields as a form
|
||||||
|
submitConf, err := stanza.NewFormSubmissionOwner(serviceName,
|
||||||
|
nodeName,
|
||||||
|
[]*stanza.Field{
|
||||||
|
fields["pubsub#max_payload_size"],
|
||||||
|
fields["pubsub#notification_type"],
|
||||||
|
})
|
||||||
|
|
||||||
|
c, _ := client.SendIQ(ctx, submitConf)
|
||||||
|
select {
|
||||||
|
case confResp := <-c:
|
||||||
|
if confResp.Type == stanza.IQTypeError {
|
||||||
|
cancel()
|
||||||
|
if vld, err := confResp.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", confResp, err)
|
||||||
|
}
|
||||||
|
log.Fatalf("node configuration failed : %s", confResp.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println("node configuration was successful")
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while configuring the node")
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while asking for the config form")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func subToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
rqSubscribe, err := stanza.NewSubRq(serviceName, stanza.SubInfo{
|
||||||
|
Node: nodeName,
|
||||||
|
Jid: userJID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
subRespCh, _ := client.SendIQ(ctx, rqSubscribe)
|
||||||
|
if subRespCh != nil {
|
||||||
|
select {
|
||||||
|
case <-subRespCh:
|
||||||
|
log.Println("Subscribed to the service")
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while subscribing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
pub, err := stanza.NewPublishItemRq(serviceName, nodeName, "", stanza.Item{
|
||||||
|
Publisher: "testuser2",
|
||||||
|
Any: &stanza.Node{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://www.w3.org/2005/Atom",
|
||||||
|
Local: "entry",
|
||||||
|
},
|
||||||
|
Nodes: []stanza.Node{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "title"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item title",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "summary"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item content summary",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "link"},
|
||||||
|
Attrs: []xml.Attr{
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "rel"},
|
||||||
|
Value: "alternate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "type"},
|
||||||
|
Value: "text/html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "href"},
|
||||||
|
Value: "http://denmark.lit/2003/12/13/atom03",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "id"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item content ID",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "published"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "2003-12-13T18:30:02Z",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "updated"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "2003-12-13T18:30:02Z",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
pubRespCh, _ := client.SendIQ(ctx, pub)
|
||||||
|
if pubRespCh != nil {
|
||||||
|
select {
|
||||||
|
case <-pubRespCh:
|
||||||
|
log.Println("Published item to the service")
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while publishing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ func main() {
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
router.HandleFunc("message", handleMessage)
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%+v", err)
|
log.Fatalf("%+v", err)
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,10 @@ func main() {
|
||||||
log.Fatal(cm.Run())
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
msg, ok := p.(stanza.Message)
|
msg, ok := p.(stanza.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
16
auth.go
16
auth.go
|
@ -60,7 +60,21 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str
|
||||||
raw := "\x00" + user + "\x00" + secret
|
raw := "\x00" + user + "\x00" + secret
|
||||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||||
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.
|
// Next message should be either success or failure.
|
||||||
val, err := stanza.NextPacket(decoder)
|
val, err := stanza.NextPacket(decoder)
|
||||||
|
|
12
bi_dir_iterator.go
Normal file
12
bi_dir_iterator.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
type BiDirIterator interface {
|
||||||
|
// Next returns the next element of this iterator, if a response is available within t milliseconds
|
||||||
|
Next(t int) (BiDirIteratorElt, error)
|
||||||
|
// Previous returns the previous element of this iterator, if a response is available within t milliseconds
|
||||||
|
Previous(t int) (BiDirIteratorElt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BiDirIteratorElt interface {
|
||||||
|
NoOp()
|
||||||
|
}
|
|
@ -79,7 +79,10 @@ func (c *ServerCheck) Check() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := f.DoesStartTLS(); ok {
|
if _, ok := f.DoesStartTLS(); ok {
|
||||||
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
_, err = fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var k stanza.TLSProceed
|
var k stanza.TLSProceed
|
||||||
if err = decoder.DecodeElement(&k, nil); err != nil {
|
if err = decoder.DecodeElement(&k, nil); err != nil {
|
||||||
|
|
249
client.go
249
client.go
|
@ -4,9 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
|
@ -15,22 +15,45 @@ import (
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// EventManager
|
// 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
|
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
|
// This is a the list of events happening on the connection that the
|
||||||
// client can be notified about.
|
// client can be notified about.
|
||||||
const (
|
const (
|
||||||
StateDisconnected ConnState = iota
|
StateDisconnected ConnState = iota
|
||||||
StateConnected
|
StateResuming
|
||||||
StateSessionEstablished
|
StateSessionEstablished
|
||||||
StateStreamError
|
StateStreamError
|
||||||
|
StatePermanentError
|
||||||
|
InitialPresence = "<presence/>"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event is a structure use to convey event changes related to client state. This
|
// 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.
|
// is for example used to notify the client when the client get disconnected.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
State ConnState
|
State SyncConnState
|
||||||
Description string
|
Description string
|
||||||
StreamError string
|
StreamError string
|
||||||
SMState SMState
|
SMState SMState
|
||||||
|
@ -43,38 +66,53 @@ type SMState struct {
|
||||||
Id string
|
Id string
|
||||||
// Inbound stanza count
|
// Inbound stanza count
|
||||||
Inbound uint
|
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
|
// TODO Store max and timestamp, to check if we should retry resumption or not
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventHandler is use to pass events about state of the connection to
|
// EventHandler is use to pass events about state of the connection to
|
||||||
// client implementation.
|
// client implementation.
|
||||||
type EventHandler func(Event)
|
type EventHandler func(Event) error
|
||||||
|
|
||||||
type EventManager struct {
|
type EventManager struct {
|
||||||
// Store current state
|
// Store current state. Please use "getState" and "setState" to access and/or modify this.
|
||||||
CurrentState ConnState
|
CurrentState SyncConnState
|
||||||
|
|
||||||
// Callback used to propagate connection state changes
|
// Callback used to propagate connection state changes
|
||||||
Handler EventHandler
|
Handler EventHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em EventManager) updateState(state ConnState) {
|
// updateState changes the CurrentState in the event manager. The state read is threadsafe but there is no guarantee
|
||||||
em.CurrentState = state
|
// regarding the triggered callback function.
|
||||||
|
func (em *EventManager) updateState(state ConnState) {
|
||||||
|
em.CurrentState.setState(state)
|
||||||
if em.Handler != nil {
|
if em.Handler != nil {
|
||||||
em.Handler(Event{State: em.CurrentState})
|
em.Handler(Event{State: em.CurrentState})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em EventManager) disconnected(state SMState) {
|
// disconnected changes the CurrentState in the event manager to "disconnected". The state read is threadsafe but there is no guarantee
|
||||||
em.CurrentState = StateDisconnected
|
// regarding the triggered callback function.
|
||||||
|
func (em *EventManager) disconnected(state SMState) {
|
||||||
|
em.CurrentState.setState(StateDisconnected)
|
||||||
if em.Handler != nil {
|
if em.Handler != nil {
|
||||||
em.Handler(Event{State: em.CurrentState, SMState: state})
|
em.Handler(Event{State: em.CurrentState, SMState: state})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em EventManager) streamError(error, desc string) {
|
// streamError changes the CurrentState in the event manager to "streamError". The state read is threadsafe but there is no guarantee
|
||||||
em.CurrentState = StateStreamError
|
// regarding the triggered callback function.
|
||||||
|
func (em *EventManager) streamError(error, desc string) {
|
||||||
|
em.CurrentState.setState(StateStreamError)
|
||||||
if em.Handler != nil {
|
if em.Handler != nil {
|
||||||
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
|
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
|
||||||
}
|
}
|
||||||
|
@ -89,7 +127,7 @@ var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ s
|
||||||
// server.
|
// server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// Store user defined options and states
|
// Store user defined options and states
|
||||||
config Config
|
config *Config
|
||||||
// Session gather data that can be accessed by users of this library
|
// Session gather data that can be accessed by users of this library
|
||||||
Session *Session
|
Session *Session
|
||||||
transport Transport
|
transport Transport
|
||||||
|
@ -97,6 +135,14 @@ type Client struct {
|
||||||
router *Router
|
router *Router
|
||||||
// Track and broadcast connection state
|
// Track and broadcast connection state
|
||||||
EventManager
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -104,11 +150,14 @@ Setting up the client / Checking the parameters
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// NewClient generates a new XMPP client, based on Config passed as 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.
|
// Default the port to 5222.
|
||||||
func NewClient(config Config, r *Router) (c *Client, err error) {
|
func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, err error) {
|
||||||
// Parse JID
|
if config.KeepaliveInterval == 0 {
|
||||||
if config.parsedJid, err = NewJid(config.Jid); err != nil {
|
config.KeepaliveInterval = time.Second * 30
|
||||||
|
}
|
||||||
|
// Parse Jid
|
||||||
|
if config.parsedJid, err = stanza.NewJid(config.Jid); err != nil {
|
||||||
err = errors.New("missing jid")
|
err = errors.New("missing jid")
|
||||||
return nil, NewConnError(err, true)
|
return nil, NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
@ -136,9 +185,15 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if config.Domain == "" {
|
||||||
|
// Fallback to jid domain
|
||||||
|
config.Domain = config.parsedJid.Domain
|
||||||
|
}
|
||||||
|
|
||||||
c = new(Client)
|
c = new(Client)
|
||||||
c.config = config
|
c.config = config
|
||||||
c.router = r
|
c.router = r
|
||||||
|
c.ErrorHandler = errorHandler
|
||||||
|
|
||||||
if c.config.ConnectTimeout == 0 {
|
if c.config.ConnectTimeout == 0 {
|
||||||
c.config.ConnectTimeout = 15 // 15 second as default
|
c.config.ConnectTimeout = 15 // 15 second as default
|
||||||
|
@ -147,7 +202,8 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
||||||
if config.TransportConfiguration.Domain == "" {
|
if config.TransportConfiguration.Domain == "" {
|
||||||
config.TransportConfiguration.Domain = config.parsedJid.Domain
|
config.TransportConfiguration.Domain = config.parsedJid.Domain
|
||||||
}
|
}
|
||||||
c.transport = NewClientTransport(config.TransportConfiguration)
|
c.config.TransportConfiguration.ConnectTimeout = c.config.ConnectTimeout
|
||||||
|
c.transport = NewClientTransport(c.config.TransportConfiguration)
|
||||||
|
|
||||||
if config.StreamLogger != nil {
|
if config.StreamLogger != nil {
|
||||||
c.transport.LogTraffic(config.StreamLogger)
|
c.transport.LogTraffic(config.StreamLogger)
|
||||||
|
@ -156,53 +212,94 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
// Connect establishes a first time connection to a XMPP server.
|
||||||
// Connect simply triggers resumption, with an empty session state.
|
// It calls the PostConnectHook
|
||||||
func (c *Client) Connect() error {
|
func (c *Client) Connect() error {
|
||||||
var state SMState
|
err := c.connect()
|
||||||
return c.Resume(state)
|
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
|
// connect establishes an actual TCP connection, based on previously defined parameters, as well as a XMPP session
|
||||||
// state.
|
func (c *Client) connect() error {
|
||||||
func (c *Client) Resume(state SMState) error {
|
var state SMState
|
||||||
var err error
|
var err error
|
||||||
|
// This is the TCP connection
|
||||||
streamId, err := c.transport.Connect()
|
streamId, err := c.transport.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.updateState(StateConnected)
|
|
||||||
|
|
||||||
// Client is ok, we now open XMPP session
|
// Client is ok, we now open XMPP session with TLS negotiation if possible and session resume or binding
|
||||||
if c.Session, err = NewSession(c.transport, c.config, state); err != nil {
|
// depending on state.
|
||||||
c.transport.Close()
|
if c.Session, err = NewSession(c, state); err != nil {
|
||||||
|
// Try to get the stream close tag from the server.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
|
if err != nil {
|
||||||
|
c.ErrorHandler(err)
|
||||||
|
c.disconnected(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch val.(type) {
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
|
||||||
|
c.transport.ReceivedStreamClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.Disconnect()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Session.StreamId = streamId
|
c.Session.StreamId = streamId
|
||||||
c.updateState(StateSessionEstablished)
|
c.updateState(StateSessionEstablished)
|
||||||
|
|
||||||
// Start the keepalive go routine
|
|
||||||
keepaliveQuit := make(chan struct{})
|
|
||||||
go keepalive(c.transport, keepaliveQuit)
|
|
||||||
// Start the receiver go routine
|
|
||||||
state = c.Session.SMState
|
|
||||||
go c.recv(state, keepaliveQuit)
|
|
||||||
|
|
||||||
// We're connected and can now receive and send messages.
|
|
||||||
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
|
||||||
// TODO: Do we always want to send initial presence automatically ?
|
|
||||||
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
|
||||||
fmt.Fprintf(c.transport, "<presence/>")
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Disconnect() {
|
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
||||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
// state. See XEP-0198
|
||||||
if c.transport != nil {
|
func (c *Client) Resume() error {
|
||||||
_ = c.transport.Close()
|
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 retrieval
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
// No transport so no connection.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetHandler(handler EventHandler) {
|
func (c *Client) SetHandler(handler EventHandler) {
|
||||||
|
@ -221,6 +318,15 @@ func (c *Client) Send(packet stanza.Packet) error {
|
||||||
return errors.New("cannot marshal packet " + err.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)
|
return c.sendWithWriter(c.transport, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,8 +339,8 @@ func (c *Client) Send(packet stanza.Packet) error {
|
||||||
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||||
// result := <- client.SendIQ(ctx, iq)
|
// 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 != "set" && iq.Attrs.Type != "get" {
|
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||||
return nil, ErrCanOnlySendGetOrSetIq
|
return nil, ErrCanOnlySendGetOrSetIq
|
||||||
}
|
}
|
||||||
if err := c.Send(iq); err != nil {
|
if err := c.Send(iq); err != nil {
|
||||||
|
@ -253,6 +359,12 @@ func (c *Client) SendRaw(packet string) error {
|
||||||
return errors.New("client is not connected")
|
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))
|
return c.sendWithWriter(c.transport, []byte(packet))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,33 +378,43 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||||
// Go routines
|
// Go routines
|
||||||
|
|
||||||
// Loop: Receive data from server
|
// Loop: Receive data from server
|
||||||
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) {
|
func (c *Client) recv(keepaliveQuit chan<- struct{}) {
|
||||||
|
defer close(keepaliveQuit)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(keepaliveQuit)
|
c.ErrorHandler(err)
|
||||||
c.disconnected(state)
|
c.disconnected(c.Session.SMState)
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stream errors
|
// Handle stream errors
|
||||||
switch packet := val.(type) {
|
switch packet := val.(type) {
|
||||||
case stanza.StreamError:
|
case stanza.StreamError:
|
||||||
c.router.route(c, val)
|
c.router.route(c, val)
|
||||||
close(keepaliveQuit)
|
|
||||||
c.streamError(packet.Error.Local, packet.Text)
|
c.streamError(packet.Error.Local, packet.Text)
|
||||||
return errors.New("stream error: " + packet.Error.Local)
|
c.ErrorHandler(errors.New("stream error: " + packet.Error.Local))
|
||||||
|
// We don't return here, because we want to wait for the stream close tag from the server, or timeout.
|
||||||
|
c.Disconnect()
|
||||||
// Process Stream management nonzas
|
// Process Stream management nonzas
|
||||||
case stanza.SMRequest:
|
case stanza.SMRequest:
|
||||||
answer := stanza.SMAnswer{XMLName: xml.Name{
|
answer := stanza.SMAnswer{XMLName: xml.Name{
|
||||||
Space: stanza.NSStreamManagement,
|
Space: stanza.NSStreamManagement,
|
||||||
Local: "a",
|
Local: "a",
|
||||||
}, H: state.Inbound}
|
}, H: c.Session.SMState.Inbound}
|
||||||
c.Send(answer)
|
err = c.Send(answer)
|
||||||
|
if err != nil {
|
||||||
|
c.ErrorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
|
||||||
|
c.transport.ReceivedStreamClose()
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
state.Inbound++
|
c.Session.SMState.Inbound++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do normal route processing in a go-routine so we can immediately
|
// Do normal route processing in a go-routine so we can immediately
|
||||||
// start receiving other stanzas. This also allows route handlers to
|
// start receiving other stanzas. This also allows route handlers to
|
||||||
// send and receive more stanzas.
|
// send and receive more stanzas.
|
||||||
|
@ -303,9 +425,8 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error)
|
||||||
// Loop: send whitespace keepalive to server
|
// Loop: send whitespace keepalive to server
|
||||||
// This is use to keep the connection open, but also to detect connection loss
|
// This is use to keep the connection open, but also to detect connection loss
|
||||||
// and trigger proper client connection shutdown.
|
// and trigger proper client connection shutdown.
|
||||||
func keepalive(transport Transport, quit <-chan struct{}) {
|
func keepalive(transport Transport, interval time.Duration, quit <-chan struct{}) {
|
||||||
// TODO: Make keepalive interval configurable
|
ticker := time.NewTicker(interval)
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
|
|
@ -2,7 +2,16 @@ package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
streamManagementID = "test-stream_management-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClient_Send(t *testing.T) {
|
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())
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
635
client_test.go
635
client_test.go
|
@ -1,10 +1,10 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -14,15 +14,34 @@ import (
|
||||||
const (
|
const (
|
||||||
// Default port is not standard XMPP port to avoid interfering
|
// Default port is not standard XMPP port to avoid interfering
|
||||||
// with local running XMPP server
|
// with local running XMPP server
|
||||||
testXMPPAddress = "localhost:15222"
|
testXMPPAddress = "localhost:15222"
|
||||||
|
testClientDomain = "localhost"
|
||||||
defaultTimeout = 2 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestEventManager(t *testing.T) {
|
||||||
|
mgr := EventManager{}
|
||||||
|
mgr.updateState(StateResuming)
|
||||||
|
if mgr.CurrentState.getState() != StateResuming {
|
||||||
|
t.Fatal("CurrentState not updated by updateState()")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.disconnected(SMState{})
|
||||||
|
|
||||||
|
if mgr.CurrentState.getState() != StateDisconnected {
|
||||||
|
t.Fatalf("CurrentState not reset by disconnected()")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.streamError(ErrTLSNotSupported.Error(), "")
|
||||||
|
|
||||||
|
if mgr.CurrentState.getState() != StateStreamError {
|
||||||
|
t.Fatalf("CurrentState not set by streamError()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClient_Connect(t *testing.T) {
|
func TestClient_Connect(t *testing.T) {
|
||||||
// Setup Mock server
|
// Setup Mock server
|
||||||
mock := ServerMock{}
|
mock := ServerMock{}
|
||||||
mock.Start(t, testXMPPAddress, handlerConnectSuccess)
|
mock.Start(t, testXMPPAddress, handlerClientConnectSuccess)
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{
|
config := Config{
|
||||||
|
@ -36,7 +55,7 @@ func TestClient_Connect(t *testing.T) {
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
if client, err = NewClient(config, router); err != nil {
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
t.Errorf("connect create XMPP client: %s", err)
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +69,10 @@ func TestClient_Connect(t *testing.T) {
|
||||||
func TestClient_NoInsecure(t *testing.T) {
|
func TestClient_NoInsecure(t *testing.T) {
|
||||||
// Setup Mock server
|
// Setup Mock server
|
||||||
mock := ServerMock{}
|
mock := ServerMock{}
|
||||||
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerAbortTLS(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
})
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{
|
config := Config{
|
||||||
|
@ -64,7 +86,7 @@ func TestClient_NoInsecure(t *testing.T) {
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
if client, err = NewClient(config, router); err != nil {
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
t.Errorf("cannot create XMPP client: %s", err)
|
t.Errorf("cannot create XMPP client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +102,10 @@ func TestClient_NoInsecure(t *testing.T) {
|
||||||
func TestClient_FeaturesTracking(t *testing.T) {
|
func TestClient_FeaturesTracking(t *testing.T) {
|
||||||
// Setup Mock server
|
// Setup Mock server
|
||||||
mock := ServerMock{}
|
mock := ServerMock{}
|
||||||
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerAbortTLS(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
})
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{
|
config := Config{
|
||||||
|
@ -94,7 +119,7 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
if client, err = NewClient(config, router); err != nil {
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
t.Errorf("cannot create XMPP client: %s", err)
|
t.Errorf("cannot create XMPP client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +134,7 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
||||||
func TestClient_RFC3921Session(t *testing.T) {
|
func TestClient_RFC3921Session(t *testing.T) {
|
||||||
// Setup Mock server
|
// Setup Mock server
|
||||||
mock := ServerMock{}
|
mock := ServerMock{}
|
||||||
mock.Start(t, testXMPPAddress, handlerConnectWithSession)
|
mock.Start(t, testXMPPAddress, handlerClientConnectWithSession)
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{
|
config := Config{
|
||||||
|
@ -124,7 +149,7 @@ func TestClient_RFC3921Session(t *testing.T) {
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
if client, err = NewClient(config, router); err != nil {
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
t.Errorf("connect create XMPP client: %s", err)
|
t.Errorf("connect create XMPP client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,56 +160,454 @@ func TestClient_RFC3921Session(t *testing.T) {
|
||||||
mock.Stop()
|
mock.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Testing sending an IQ to the mock server and reading its response.
|
||||||
|
func TestClient_SendIQ(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for Mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
discardPresence(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
client, mock := mockClientConnection(t, h, testClientIqPort)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
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
|
||||||
|
|
||||||
|
// Handle a possible error
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
client.ErrorHandler = errorHandler
|
||||||
|
res, err := client.SendIQ(ctx, iqReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-res: // If the server responds with an IQ, we pass the test
|
||||||
|
case err := <-errChan: // If the server sends an error, or there is a connection error
|
||||||
|
cancel()
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||||
|
cancel()
|
||||||
|
t.Fatal("Failed to receive response, to sent IQ, from mock server")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
mock.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
cancel()
|
||||||
|
t.Fatal("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendIQFail(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for Mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
discardPresence(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
client, mock := mockClientConnection(t, h, testClientIqFailPort)
|
||||||
|
|
||||||
|
//==================
|
||||||
|
// Create an IQ to send
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
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
|
||||||
|
// so we need to overwrite it.
|
||||||
|
iqReq.Id = ""
|
||||||
|
|
||||||
|
// Handle a possible error
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
client.ErrorHandler = errorHandler
|
||||||
|
res, _ := client.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
select {
|
||||||
|
case <-res: // If the server responds with an IQ
|
||||||
|
t.Errorf("Server should not respond with an IQ since the request is expected to be invalid !")
|
||||||
|
case <-errChan: // If the server sends an error, the test passes
|
||||||
|
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||||
|
t.Errorf("Failed to receive response, to sent IQ, from mock server")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
mock.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
cancel()
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendRaw(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for Mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
discardPresence(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
type testCase struct {
|
||||||
|
req string
|
||||||
|
shouldErr bool
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
testRequests := make(map[string]testCase)
|
||||||
|
// Sending a correct IQ of type get. Not supposed to err
|
||||||
|
testRequests["Correct IQ"] = testCase{
|
||||||
|
req: `<iq type="get" id="91bd0bba-012f-4d92-bb17-5fc41e6fe545" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: false,
|
||||||
|
port: testClientRawPort + 100,
|
||||||
|
}
|
||||||
|
// Sending an IQ with a missing ID. Should err
|
||||||
|
testRequests["IQ with missing ID"] = testCase{
|
||||||
|
req: `<iq type="get" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: true,
|
||||||
|
port: testClientRawPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A handler for the client.
|
||||||
|
// In the failing test, the server returns a stream error, which triggers this handler, client side.
|
||||||
|
errChan := make(chan error)
|
||||||
|
errHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for all the IQs
|
||||||
|
for name, tcase := range testRequests {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockClientConnection(t, h, tcase.port)
|
||||||
|
c.ErrorHandler = errHandler
|
||||||
|
// Sending raw xml from test case
|
||||||
|
err := c.SendRaw(tcase.req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error sending Raw string")
|
||||||
|
}
|
||||||
|
// Just wait a little so the message has time to arrive
|
||||||
|
select {
|
||||||
|
// We don't use the default "long" timeout here because waiting it out means passing the test.
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
c.Disconnect()
|
||||||
|
case err = <-errChan:
|
||||||
|
if err == nil && tcase.shouldErr {
|
||||||
|
t.Errorf("Failed to get closing stream err")
|
||||||
|
} else if err != nil && !tcase.shouldErr {
|
||||||
|
t.Errorf("This test is not supposed to err !")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
m.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_Disconnect(t *testing.T) {
|
||||||
|
c, m := mockClientConnection(t, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerClientConnectSuccess(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
}, testClientBasePort)
|
||||||
|
err := c.transport.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not ping but not disconnected yet")
|
||||||
|
}
|
||||||
|
c.Disconnect()
|
||||||
|
err = c.transport.Ping()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Did not disconnect properly")
|
||||||
|
}
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DisconnectStreamManager(t *testing.T) {
|
||||||
|
// Init mock server
|
||||||
|
// Setup Mock server
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerAbortTLS(t, sc)
|
||||||
|
closeConn(t, sc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test / Check result
|
||||||
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *Client
|
||||||
|
var err error
|
||||||
|
router := NewRouter()
|
||||||
|
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||||
|
t.Errorf("cannot create XMPP client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sman := NewStreamManager(client, nil)
|
||||||
|
errChan := make(chan error)
|
||||||
|
runSMan := func(errChan chan error) {
|
||||||
|
errChan <- sman.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
go runSMan(errChan)
|
||||||
|
select {
|
||||||
|
case <-errChan:
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
// When insecure is not allowed:
|
||||||
|
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
|
||||||
|
}
|
||||||
|
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.
|
// Basic XMPP Server Mock Handlers.
|
||||||
|
|
||||||
const serverStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' id='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
|
||||||
|
|
||||||
// Test connection with a basic straightforward workflow
|
// Test connection with a basic straightforward workflow
|
||||||
func handlerConnectSuccess(t *testing.T, c net.Conn) {
|
func handlerClientConnectSuccess(t *testing.T, sc *ServerConn) {
|
||||||
decoder := xml.NewDecoder(c)
|
checkClientOpenStream(t, sc)
|
||||||
checkOpenStream(t, c, decoder)
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
|
readAuth(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
sendStreamFeatures(t, c, decoder) // Send initial features
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
readAuth(t, decoder)
|
sendBindFeature(t, sc) // Send post auth features
|
||||||
fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
|
bind(t, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeConn closes the connection on request from the client
|
||||||
|
func closeConn(t *testing.T, sc *ServerConn) {
|
||||||
|
for {
|
||||||
|
cls, err := stanza.NextPacket(sc.decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read from socket: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch cls.(type) {
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
sc.connection.Write([]byte(stanza.StreamClose))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkOpenStream(t, c, decoder) // Reset stream
|
|
||||||
sendBindFeature(t, c, decoder) // Send post auth features
|
|
||||||
bind(t, c, decoder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We expect client will abort on TLS
|
// We expect client will abort on TLS
|
||||||
func handlerAbortTLS(t *testing.T, c net.Conn) {
|
func handlerAbortTLS(t *testing.T, sc *ServerConn) {
|
||||||
decoder := xml.NewDecoder(c)
|
checkClientOpenStream(t, sc)
|
||||||
checkOpenStream(t, c, decoder)
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
sendStreamFeatures(t, c, decoder) // Send initial features
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test connection with mandatory session (RFC-3921)
|
// Test connection with mandatory session (RFC-3921)
|
||||||
func handlerConnectWithSession(t *testing.T, c net.Conn) {
|
func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) {
|
||||||
decoder := xml.NewDecoder(c)
|
checkClientOpenStream(t, sc)
|
||||||
checkOpenStream(t, c, decoder)
|
|
||||||
|
|
||||||
sendStreamFeatures(t, c, decoder) // Send initial features
|
sendStreamFeatures(t, sc) // Send initial features
|
||||||
readAuth(t, decoder)
|
readAuth(t, sc.decoder)
|
||||||
fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
|
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||||
|
|
||||||
checkOpenStream(t, c, decoder) // Reset stream
|
checkClientOpenStream(t, sc) // Reset stream
|
||||||
sendRFC3921Feature(t, c, decoder) // Send post auth features
|
sendRFC3921Feature(t, sc) // Send post auth features
|
||||||
bind(t, c, decoder)
|
bind(t, sc)
|
||||||
session(t, c, decoder)
|
session(t, sc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
func checkClientOpenStream(t *testing.T, sc *ServerConn) {
|
||||||
c.SetDeadline(time.Now().Add(defaultTimeout))
|
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
|
||||||
defer c.SetDeadline(time.Time{})
|
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.
|
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
|
||||||
var token xml.Token
|
var token xml.Token
|
||||||
token, err := decoder.Token()
|
token, err := sc.decoder.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("cannot read next token: %s", err)
|
t.Fatalf("cannot read next token: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch elem := token.(type) {
|
switch elem := token.(type) {
|
||||||
|
@ -194,113 +617,43 @@ func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||||
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
|
if _, err := fmt.Fprintf(sc.connection, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
|
||||||
t.Errorf("cannot write server stream open: %s", err)
|
t.Errorf("cannot write server stream open: %s", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int) (*Client, *ServerMock) {
|
||||||
// This is a basic server, supporting only 1 stream feature: SASL Plain Auth
|
mock := &ServerMock{}
|
||||||
features := `<stream:features>
|
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
|
||||||
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
|
||||||
<mechanism>PLAIN</mechanism>
|
mock.Start(t, testServerAddress, serverHandler)
|
||||||
</mechanisms>
|
config := Config{
|
||||||
</stream:features>`
|
TransportConfiguration: TransportConfiguration{
|
||||||
if _, err := fmt.Fprintln(c, features); err != nil {
|
Address: testServerAddress,
|
||||||
t.Errorf("cannot send stream feature: %s", err)
|
},
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
t.Errorf("XMPP connection failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return err in case of error reading the auth params
|
// This really should not be used as is.
|
||||||
func readAuth(t *testing.T, decoder *xml.Decoder) string {
|
// It's just meant to be a placeholder when error handling is not needed at this level
|
||||||
se, err := stanza.NextStart(decoder)
|
func clientDefaultErrorHandler(err error) {
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read auth: %s", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var nv interface{}
|
|
||||||
nv = &stanza.SASLAuth{}
|
|
||||||
// Decode element into pointer storage
|
|
||||||
if err = decoder.DecodeElement(nv, &se); err != nil {
|
|
||||||
t.Errorf("cannot decode auth: %s", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := nv.(type) {
|
|
||||||
case *stanza.SASLAuth:
|
|
||||||
return v.Value
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
|
||||||
// This is a basic server, supporting only 1 stream feature after auth: resource binding
|
|
||||||
features := `<stream:features>
|
|
||||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
|
||||||
</stream:features>`
|
|
||||||
if _, err := fmt.Fprintln(c, features); err != nil {
|
|
||||||
t.Errorf("cannot send stream feature: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
|
||||||
// This is a basic server, supporting only 2 features after auth: resource & session binding
|
|
||||||
features := `<stream:features>
|
|
||||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
|
||||||
<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
|
|
||||||
</stream:features>`
|
|
||||||
if _, err := fmt.Fprintln(c, features); err != nil {
|
|
||||||
t.Errorf("cannot send stream feature: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
|
||||||
se, err := stanza.NextStart(decoder)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read bind: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iq := &stanza.IQ{}
|
|
||||||
// Decode element into pointer storage
|
|
||||||
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
|
||||||
t.Errorf("cannot decode bind iq: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Check all elements
|
|
||||||
switch iq.Payload.(type) {
|
|
||||||
case *stanza.Bind:
|
|
||||||
result := `<iq id='%s' type='result'>
|
|
||||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
|
|
||||||
<jid>%s</jid>
|
|
||||||
</bind>
|
|
||||||
</iq>`
|
|
||||||
fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func session(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
|
||||||
se, err := stanza.NextStart(decoder)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cannot read session: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iq := &stanza.IQ{}
|
|
||||||
// Decode element into pointer storage
|
|
||||||
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
|
||||||
t.Errorf("cannot decode session iq: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch iq.Payload.(type) {
|
|
||||||
case *stanza.StreamSession:
|
|
||||||
result := `<iq id='%s' type='result'/>`
|
|
||||||
fmt.Fprintf(c, result, iq.Id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -31,13 +32,17 @@ func sendxmpp(cmd *cobra.Command, args []string) {
|
||||||
msgText := args[1]
|
msgText := args[1]
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
client, err := xmpp.NewClient(xmpp.Config{
|
client, err := xmpp.NewClient(&xmpp.Config{
|
||||||
TransportConfiguration: xmpp.TransportConfiguration{
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
Address: viper.GetString("addr"),
|
Address: viper.GetString("addr"),
|
||||||
},
|
},
|
||||||
Jid: viper.GetString("jid"),
|
Jid: viper.GetString("jid"),
|
||||||
Credential: xmpp.Password(viper.GetString("password")),
|
Credential: xmpp.Password(viper.GetString("password")),
|
||||||
}, xmpp.NewRouter())
|
},
|
||||||
|
xmpp.NewRouter(),
|
||||||
|
func(err error) {
|
||||||
|
log.Println(err)
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error when starting xmpp client: %s", err)
|
log.Errorf("error when starting xmpp client: %s", err)
|
||||||
|
@ -48,7 +53,7 @@ func sendxmpp(cmd *cobra.Command, args []string) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
// FIXME: Remove global variables
|
// FIXME: Remove global variables
|
||||||
var mucsToLeave []*xmpp.Jid
|
var mucsToLeave []*stanza.Jid
|
||||||
|
|
||||||
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
|
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
@ -57,7 +62,7 @@ func sendxmpp(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
if isMUCRecipient {
|
if isMUCRecipient {
|
||||||
for _, muc := range receiver {
|
for _, muc := range receiver {
|
||||||
jid, err := xmpp.NewJid(muc)
|
jid, err := stanza.NewJid(muc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err)
|
log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
|
|
||||||
func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error {
|
func joinMUC(c xmpp.Sender, toJID *stanza.Jid) error {
|
||||||
return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()},
|
return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()},
|
||||||
Extensions: []stanza.PresExtension{
|
Extensions: []stanza.PresExtension{
|
||||||
stanza.MucPresence{
|
stanza.MucPresence{
|
||||||
|
@ -16,7 +16,7 @@ func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func leaveMUCs(c xmpp.Sender, mucsToLeave []*xmpp.Jid) {
|
func leaveMUCs(c xmpp.Sender, mucsToLeave []*stanza.Jid) {
|
||||||
for _, muc := range mucsToLeave {
|
for _, muc := range mucsToLeave {
|
||||||
if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{
|
if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{
|
||||||
To: muc.Full(),
|
To: muc.Full(),
|
||||||
|
|
|
@ -6,7 +6,7 @@ require (
|
||||||
github.com/bdlm/log v0.1.19
|
github.com/bdlm/log v0.1.19
|
||||||
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
github.com/spf13/viper v1.4.0
|
github.com/spf13/viper v1.6.1
|
||||||
gosrc.io/xmpp v0.1.1
|
gosrc.io/xmpp v0.1.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
23
cmd/go.sum
23
cmd/go.sum
|
@ -6,6 +6,8 @@ github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2e
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/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/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/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 h1:GqVFZC+khJCEbtTmkaDL/araNDwxTeLBmdMK8pbRoBE=
|
||||||
github.com/bdlm/log v0.1.19/go.mod h1:30V5Zwc5Vt5ePq5rd9KJ6JQ/A5aFUcKzq5fYtO7c9qc=
|
github.com/bdlm/log v0.1.19/go.mod h1:30V5Zwc5Vt5ePq5rd9KJ6JQ/A5aFUcKzq5fYtO7c9qc=
|
||||||
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 h1:ggZyn+N8eoBh/qLla2kUtqm/ysjnkbzUxTQY+6LMshY=
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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/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.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-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
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/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-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/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/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/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-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/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 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
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/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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/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/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 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
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-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-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/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.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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
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/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/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
github.com/mitchellh/mapstructure v1.1.2 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.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
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/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/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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
github.com/spf13/afero v1.1.2 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.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
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.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/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.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 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/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 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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/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/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=
|
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-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-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-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/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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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-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-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-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/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 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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/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/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/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/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/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.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.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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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 v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
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=
|
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=
|
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=
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
comment: off
|
|
|
@ -1,5 +0,0 @@
|
||||||
build:
|
|
||||||
build:
|
|
||||||
image: fluux/build
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
encrypted_env_file: codeship.env.encrypted
|
|
|
@ -1,5 +0,0 @@
|
||||||
- type: serial
|
|
||||||
steps:
|
|
||||||
- name: test
|
|
||||||
service: build
|
|
||||||
command: ./test.sh
|
|
|
@ -1 +0,0 @@
|
||||||
yVKgVFeKW6SSnC/KgLYpfYtTcqqTke1gOIW5GUiVvRijnhweOJiYKFPmwPjpt1FVrg4WVELQUNbxn3lmfyHVVF7r
|
|
73
component.go
73
component.go
|
@ -7,9 +7,8 @@ import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ComponentOptions struct {
|
type ComponentOptions struct {
|
||||||
|
@ -49,22 +48,22 @@ type Component struct {
|
||||||
transport Transport
|
transport Transport
|
||||||
|
|
||||||
// read / write
|
// read / write
|
||||||
socketProxy io.ReadWriter // TODO
|
socketProxy io.ReadWriter // TODO
|
||||||
decoder *xml.Decoder
|
ErrorHandler func(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
|
func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*Component, error) {
|
||||||
c := Component{ComponentOptions: opts, router: r}
|
c := Component{ComponentOptions: opts, router: r, ErrorHandler: errorHandler}
|
||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect triggers component connection to XMPP server component port.
|
// Connect triggers component connection to XMPP server component port.
|
||||||
// TODO: Failed handshake should be a permanent error
|
// TODO: Failed handshake should be a permanent error
|
||||||
func (c *Component) Connect() error {
|
func (c *Component) Connect() error {
|
||||||
var state SMState
|
return c.Resume()
|
||||||
return c.Resume(state)
|
|
||||||
}
|
}
|
||||||
func (c *Component) Resume(sm SMState) error {
|
|
||||||
|
func (c *Component) Resume() error {
|
||||||
var err error
|
var err error
|
||||||
var streamId string
|
var streamId string
|
||||||
if c.ComponentOptions.TransportConfiguration.Domain == "" {
|
if c.ComponentOptions.TransportConfiguration.Domain == "" {
|
||||||
|
@ -72,26 +71,26 @@ func (c *Component) Resume(sm SMState) error {
|
||||||
}
|
}
|
||||||
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.updateState(StateStreamError)
|
c.updateState(StatePermanentError)
|
||||||
return err
|
return NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if streamId, err = c.transport.Connect(); err != nil {
|
if streamId, err = c.transport.Connect(); err != nil {
|
||||||
c.updateState(StateStreamError)
|
c.updateState(StatePermanentError)
|
||||||
return err
|
return NewConnError(err, true)
|
||||||
}
|
}
|
||||||
c.updateState(StateConnected)
|
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
if _, err := fmt.Fprintf(c.transport, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
|
if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("<handshake>%s</handshake>", c.handshake(streamId)))); err != nil {
|
||||||
c.updateState(StateStreamError)
|
c.updateState(StateStreamError)
|
||||||
|
|
||||||
return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
|
return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check server response for authentication
|
// Check server response for authentication
|
||||||
val, err := stanza.NextPacket(c.decoder)
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.updateState(StateDisconnected)
|
c.updateState(StatePermanentError)
|
||||||
return NewConnError(err, true)
|
return NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,18 +102,20 @@ func (c *Component) Resume(sm SMState) error {
|
||||||
// Start the receiver go routine
|
// Start the receiver go routine
|
||||||
c.updateState(StateSessionEstablished)
|
c.updateState(StateSessionEstablished)
|
||||||
go c.recv()
|
go c.recv()
|
||||||
return nil
|
return err // Should be empty at this point
|
||||||
default:
|
default:
|
||||||
c.updateState(StateStreamError)
|
c.updateState(StatePermanentError)
|
||||||
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
|
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Component) Disconnect() {
|
func (c *Component) Disconnect() error {
|
||||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
||||||
if c.transport != nil {
|
if c.transport != nil {
|
||||||
_ = c.transport.Close()
|
return c.transport.Close()
|
||||||
}
|
}
|
||||||
|
// No transport so no connection.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Component) SetHandler(handler EventHandler) {
|
func (c *Component) SetHandler(handler EventHandler) {
|
||||||
|
@ -122,20 +123,26 @@ func (c *Component) SetHandler(handler EventHandler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receiver Go routine receiver
|
// Receiver Go routine receiver
|
||||||
func (c *Component) recv() (err error) {
|
func (c *Component) recv() {
|
||||||
for {
|
for {
|
||||||
val, err := stanza.NextPacket(c.decoder)
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.updateState(StateDisconnected)
|
c.updateState(StateDisconnected)
|
||||||
return err
|
c.ErrorHandler(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stream errors
|
// Handle stream errors
|
||||||
switch p := val.(type) {
|
switch p := val.(type) {
|
||||||
case stanza.StreamError:
|
case stanza.StreamError:
|
||||||
c.router.route(c, val)
|
c.router.route(c, val)
|
||||||
c.streamError(p.Error.Local, p.Text)
|
c.streamError(p.Error.Local, p.Text)
|
||||||
return errors.New("stream error: " + p.Error.Local)
|
c.ErrorHandler(errors.New("stream error: " + p.Error.Local))
|
||||||
|
// We don't return here, because we want to wait for the stream close tag from the server, or timeout.
|
||||||
|
c.Disconnect()
|
||||||
|
case stanza.StreamClosePacket:
|
||||||
|
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
|
||||||
|
c.transport.ReceivedStreamClose()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.router.route(c, val)
|
c.router.route(c, val)
|
||||||
}
|
}
|
||||||
|
@ -153,12 +160,18 @@ func (c *Component) Send(packet stanza.Packet) error {
|
||||||
return errors.New("cannot marshal packet " + err.Error())
|
return errors.New("cannot marshal packet " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fmt.Fprintf(transport, string(data)); err != nil {
|
if err := c.sendWithWriter(transport, data); err != nil {
|
||||||
return errors.New("cannot send packet " + err.Error())
|
return errors.New("cannot send packet " + err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Component) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||||
|
var err error
|
||||||
|
_, err = writer.Write(packet)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// SendIQ sends an IQ set or get stanza to the server. If a result is received
|
// SendIQ sends an IQ set or get stanza to the server. If a result is received
|
||||||
// the provided handler function will automatically be called.
|
// the provided handler function will automatically be called.
|
||||||
//
|
//
|
||||||
|
@ -168,8 +181,8 @@ func (c *Component) Send(packet stanza.Packet) error {
|
||||||
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||||
// result := <- client.SendIQ(ctx, iq)
|
// 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 != "set" && iq.Attrs.Type != "get" {
|
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||||
return nil, ErrCanOnlySendGetOrSetIq
|
return nil, ErrCanOnlySendGetOrSetIq
|
||||||
}
|
}
|
||||||
if err := c.Send(iq); err != nil {
|
if err := c.Send(iq); err != nil {
|
||||||
|
@ -189,7 +202,7 @@ func (c *Component) SendRaw(packet string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
_, err = fmt.Fprintf(transport, packet)
|
err = c.sendWithWriter(transport, []byte(packet))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tests are ran in parallel, so each test creating a server must use a different port so we do not get any
|
||||||
|
// conflict. Using iota for this should do the trick.
|
||||||
|
const (
|
||||||
|
defaultChannelTimeout = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandshake(t *testing.T) {
|
func TestHandshake(t *testing.T) {
|
||||||
|
@ -20,8 +35,103 @@ func TestHandshake(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateHandshake(t *testing.T) {
|
// Tests connection process with a handshake exchange
|
||||||
// TODO
|
// 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
|
||||||
|
var uuidsArray = [5]string{}
|
||||||
|
for i := 1; i < len(uuidsArray); i++ {
|
||||||
|
id, _ := uuid.NewRandom()
|
||||||
|
uuidsArray[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel to pass stream IDs as a queue
|
||||||
|
var uchan = make(chan string, len(uuidsArray))
|
||||||
|
// Populate test channel
|
||||||
|
for _, elt := range uuidsArray {
|
||||||
|
uchan <- elt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
sc.connection.Write([]byte("<handshake/>")) // That's all the server needs to return (see xep-0114)
|
||||||
|
serverDone <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddess := fmt.Sprintf("%s:%d", testComponentDomain, testHandshakePort)
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testComponentAddess, h)
|
||||||
|
|
||||||
|
// Init component
|
||||||
|
opts := ComponentOptions{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testComponentAddess,
|
||||||
|
Domain: "localhost",
|
||||||
|
},
|
||||||
|
Domain: testComponentDomain,
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: "Test Component",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
router := NewRouter()
|
||||||
|
c, err := NewComponent(opts, router, componentDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try connecting, and storing the resulting streamID in a map.
|
||||||
|
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.
|
// Test that NewStreamManager can accept a Component.
|
||||||
|
@ -30,3 +140,373 @@ func TestGenerateHandshake(t *testing.T) {
|
||||||
func TestStreamManager(t *testing.T) {
|
func TestStreamManager(t *testing.T) {
|
||||||
NewStreamManager(&Component{}, nil)
|
NewStreamManager(&Component{}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that the decoder is properly initialized when connecting a component to a server.
|
||||||
|
// The decoder is expected to be built after a valid connection
|
||||||
|
// Based on the xmpp_component example.
|
||||||
|
func TestDecoder(t *testing.T) {
|
||||||
|
c, _ := mockComponentConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID)
|
||||||
|
if c.transport.GetDecoder() == nil {
|
||||||
|
t.Errorf("Failed to initialize decoder. Decoder is nil.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests sending an IQ to the server, and getting the response
|
||||||
|
func TestSendIq(t *testing.T) {
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
clientDone := make(chan struct{})
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerForComponentIQSend(t, sc)
|
||||||
|
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, 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
|
||||||
|
|
||||||
|
// Handle a possible error
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
c.ErrorHandler = errorHandler
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var res chan stanza.IQ
|
||||||
|
res, _ = c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-res:
|
||||||
|
case err := <-errChan:
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
clientDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitForEntity(t, clientDone)
|
||||||
|
waitForEntity(t, serverDone)
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking that error handling is done properly client side when an invalid IQ is sent and the server responds in kind.
|
||||||
|
func TestSendIqFail(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
handlerForComponentIQSend(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockComponentConnection(t, testSendIqFailPort, h)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
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.
|
||||||
|
iqReq.Id = ""
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
|
||||||
|
errChan := make(chan error)
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
c.ErrorHandler = errorHandler
|
||||||
|
|
||||||
|
var res chan stanza.IQ
|
||||||
|
res, _ = c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case r := <-res: // Do we get an IQ response from the server ?
|
||||||
|
t.Errorf("We should not be getting an IQ response here : this should fail !")
|
||||||
|
fmt.Println(r)
|
||||||
|
case <-errChan: // Do we get a stream error from the server ?
|
||||||
|
// If we get an error from the server, the test passes.
|
||||||
|
case <-time.After(defaultChannelTimeout): // Timeout ?
|
||||||
|
t.Errorf("Failed to receive response, to sent IQ, from mock server")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
m.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests sending raw xml to the mock server.
|
||||||
|
// Right now, the server response is not checked and an err is passed in a channel if the test is supposed to err.
|
||||||
|
// In this test, we use IQs
|
||||||
|
func TestSendRaw(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
// Handler for the mock server
|
||||||
|
h := func(t *testing.T, sc *ServerConn) {
|
||||||
|
// Completes the connection by exchanging handshakes
|
||||||
|
handlerForComponentHandshakeDefaultID(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
req string
|
||||||
|
shouldErr bool
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
testRequests := make(map[string]testCase)
|
||||||
|
// Sending a correct IQ of type get. Not supposed to err
|
||||||
|
testRequests["Correct IQ"] = testCase{
|
||||||
|
req: `<iq type="get" id="91bd0bba-012f-4d92-bb17-5fc41e6fe545" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: false,
|
||||||
|
port: testSendRawPort + 100,
|
||||||
|
}
|
||||||
|
// Sending an IQ with a missing ID. Should err
|
||||||
|
testRequests["IQ with missing ID"] = testCase{
|
||||||
|
req: `<iq type="get" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: true,
|
||||||
|
port: testSendRawPort + 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A handler for the component.
|
||||||
|
// In the failing test, the server returns a stream error, which triggers this handler, component side.
|
||||||
|
errChan := make(chan error)
|
||||||
|
errHandler := func(err error) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for all the IQs
|
||||||
|
for name, tcase := range testRequests {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockComponentConnection(t, tcase.port, h)
|
||||||
|
c.ErrorHandler = errHandler
|
||||||
|
// Sending raw xml from test case
|
||||||
|
err := c.SendRaw(tcase.req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error sending Raw string")
|
||||||
|
}
|
||||||
|
// Just wait a little so the message has time to arrive
|
||||||
|
select {
|
||||||
|
// We don't use the default "long" timeout here because waiting it out means passing the test.
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
case err = <-errChan:
|
||||||
|
if err == nil && tcase.shouldErr {
|
||||||
|
t.Errorf("Failed to get closing stream err")
|
||||||
|
} else if err != nil && !tcase.shouldErr {
|
||||||
|
t.Errorf("This test is not supposed to err ! => %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.transport.Close()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
m.Stop()
|
||||||
|
case <-time.After(defaultChannelTimeout):
|
||||||
|
t.Errorf("The mock server failed to finish its job !")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests the Disconnect method for Components
|
||||||
|
func TestDisconnect(t *testing.T) {
|
||||||
|
c, m := mockComponentConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID)
|
||||||
|
err := c.transport.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not ping but not disconnected yet")
|
||||||
|
}
|
||||||
|
c.Disconnect()
|
||||||
|
err = c.transport.Ping()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Did not disconnect properly")
|
||||||
|
}
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that a streamManager successfully disconnects when a handshake fails between the component and the server.
|
||||||
|
func TestStreamManagerDisconnect(t *testing.T) {
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, testSManDisconnectPort)
|
||||||
|
mock := ServerMock{}
|
||||||
|
// Handler fails the handshake, which is currently the only option to disconnect completely when using a streamManager
|
||||||
|
// a failed handshake being a permanent error, except for a "conflict"
|
||||||
|
mock.Start(t, testComponentAddress, handlerComponentFailedHandshakeDefaultID)
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// Create Component to connect to it
|
||||||
|
c := makeBasicComponent(defaultComponentName, testComponentAddress, t)
|
||||||
|
|
||||||
|
//========================================
|
||||||
|
// Connect the new Component to the server
|
||||||
|
cm := NewStreamManager(c, nil)
|
||||||
|
errChan := make(chan error)
|
||||||
|
runSMan := func(errChan chan error) {
|
||||||
|
errChan <- cm.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
go runSMan(errChan)
|
||||||
|
select {
|
||||||
|
case <-errChan:
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("The component and server seem to still be connected while they should not.")
|
||||||
|
}
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Basic XMPP Server Mock Handlers.
|
||||||
|
|
||||||
|
//===============================
|
||||||
|
// Init mock server and connection
|
||||||
|
// Creating a mock server and connecting a Component to it. Initialized with given port and handler function
|
||||||
|
// The Component and mock are both returned
|
||||||
|
func mockComponentConnection(t *testing.T, port int, handler func(t *testing.T, sc *ServerConn)) (*Component, *ServerMock) {
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port)
|
||||||
|
mock := &ServerMock{}
|
||||||
|
mock.Start(t, testComponentAddress, handler)
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// Create Component to connect to it
|
||||||
|
c := makeBasicComponent(defaultComponentName, testComponentAddress, t)
|
||||||
|
|
||||||
|
//========================================
|
||||||
|
// Connect the new Component to the server
|
||||||
|
err := c.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the Component is connected, let's set the xml.Decoder for the server
|
||||||
|
|
||||||
|
return c, mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component {
|
||||||
|
opts := ComponentOptions{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: mockServerAddr,
|
||||||
|
Domain: "localhost",
|
||||||
|
},
|
||||||
|
Domain: testComponentDomain,
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: name,
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
router := NewRouter()
|
||||||
|
c, err := NewComponent(opts, router, componentDefaultErrorHandler)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// This really should not be used as is.
|
||||||
|
// It's just meant to be a placeholder when error handling is not needed at this level
|
||||||
|
func componentDefaultErrorHandler(err error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends IQ response to Component request.
|
||||||
|
// No parsing of the request here. We just check that it's valid, and send the default response.
|
||||||
|
func handlerForComponentIQSend(t *testing.T, sc *ServerConn) {
|
||||||
|
// Completes the connection by exchanging handshakes
|
||||||
|
handlerForComponentHandshakeDefaultID(t, sc)
|
||||||
|
respondToIQ(t, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for ID and handshake related tests
|
||||||
|
func checkOpenStreamHandshakeID(t *testing.T, sc *ServerConn, streamID string) {
|
||||||
|
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.
|
||||||
|
token, err := sc.decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read next token: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch elem := token.(type) {
|
||||||
|
// Wait for first startElement
|
||||||
|
case xml.StartElement:
|
||||||
|
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
|
||||||
|
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(sc.connection, serverStreamOpen, "localhost", streamID, stanza.NSComponent, stanza.NSStream); err != nil {
|
||||||
|
t.Errorf("cannot write server stream open: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOpenStreamHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||||
|
checkOpenStreamHandshakeID(t, sc, defaultStreamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant.
|
||||||
|
// This handler is supposed to fail by sending a "message" stanza instead of a <handshake/> stanza to finalize the handshake.
|
||||||
|
func handlerComponentFailedHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||||
|
checkOpenStreamHandshakeDefaultID(t, sc)
|
||||||
|
readHandshakeComponent(t, sc.decoder)
|
||||||
|
|
||||||
|
// Send a message, instead of a "<handshake/>" tag, to fail the handshake process dans disconnect the client.
|
||||||
|
me := stanza.Message{
|
||||||
|
Attrs: stanza.Attrs{Type: stanza.MessageTypeChat, From: defaultServerName, To: defaultComponentName, Lang: "en"},
|
||||||
|
Body: "Fail my handshake.",
|
||||||
|
}
|
||||||
|
s, _ := xml.Marshal(me)
|
||||||
|
_, err := sc.connection.Write(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not write message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads from the connection with the Component. Expects a handshake request, and returns the <handshake/> tag.
|
||||||
|
func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) {
|
||||||
|
se, err := stanza.NextStart(decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read auth: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nv := &stanza.Handshake{}
|
||||||
|
// Decode element into pointer storage
|
||||||
|
if err = decoder.DecodeElement(nv, &se); err != nil {
|
||||||
|
t.Errorf("cannot decode handshake: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(strings.TrimSpace(nv.Value)) == 0 {
|
||||||
|
t.Errorf("did not receive handshake ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant.
|
||||||
|
// Used in the mock server as a Handler
|
||||||
|
func handlerForComponentHandshakeDefaultID(t *testing.T, sc *ServerConn) {
|
||||||
|
checkOpenStreamHandshakeDefaultID(t, sc)
|
||||||
|
readHandshakeComponent(t, sc.decoder)
|
||||||
|
sc.connection.Write([]byte("<handshake/>")) // That's all the server needs to return (see xep-0114)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
26
config.go
26
config.go
|
@ -1,7 +1,9 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config & TransportConfiguration must not be modified after having been passed to NewClient. Any
|
// Config & TransportConfiguration must not be modified after having been passed to NewClient. Any
|
||||||
|
@ -9,13 +11,25 @@ import (
|
||||||
type Config struct {
|
type Config struct {
|
||||||
TransportConfiguration
|
TransportConfiguration
|
||||||
|
|
||||||
Jid string
|
Jid string
|
||||||
parsedJid *Jid // For easier manipulation
|
parsedJid *stanza.Jid // For easier manipulation
|
||||||
Credential Credential
|
Credential Credential
|
||||||
StreamLogger *os.File // Used for debugging
|
StreamLogger *os.File // Used for debugging
|
||||||
Lang string // TODO: should default to 'en'
|
Lang string // TODO: should default to 'en'
|
||||||
ConnectTimeout int // Client timeout in seconds. Default to 15
|
KeepaliveInterval time.Duration // Interval between keepalive packets
|
||||||
|
ConnectTimeout int // Client timeout in seconds. Default to 15
|
||||||
// Insecure can be set to true to allow to open a session without TLS. If TLS
|
// 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.
|
// is supported on the server, we will still try to use it.
|
||||||
Insecure bool
|
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
|
||||||
}
|
}
|
||||||
|
|
2
doc.go
2
doc.go
|
@ -29,7 +29,7 @@ Components
|
||||||
|
|
||||||
XMPP components can typically be used to extends the features of an XMPP
|
XMPP components can typically be used to extends the features of an XMPP
|
||||||
server, in a portable way, using component protocol over persistent TCP
|
server, in a portable way, using component protocol over persistent TCP
|
||||||
connections.
|
serverConnections.
|
||||||
|
|
||||||
Component protocol is defined in XEP-114 (https://xmpp.org/extensions/xep-0114.html).
|
Component protocol is defined in XEP-114 (https://xmpp.org/extensions/xep-0114.html).
|
||||||
|
|
||||||
|
|
104
go.sum
104
go.sum
|
@ -1,23 +1,55 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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.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-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
@ -27,14 +59,26 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
|
||||||
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
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=
|
||||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
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-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-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
@ -45,36 +89,83 @@ 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.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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
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/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
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/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/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/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/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
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/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=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -85,22 +176,35 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/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/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 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/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/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
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/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
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=
|
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 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||||
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
|
|
|
@ -23,7 +23,7 @@ func ensurePort(addr string, port int) string {
|
||||||
// This is IPV4 without port
|
// This is IPV4 without port
|
||||||
return addr + ":" + strconv.Itoa(port)
|
return addr + ":" + strconv.Itoa(port)
|
||||||
case 1:
|
case 1:
|
||||||
// This is IPV$ with port
|
// This is IPV6 with port
|
||||||
return addr
|
return addr
|
||||||
default:
|
default:
|
||||||
// This is IPV6 without port, as you need to use bracket with port in IPV6
|
// This is IPV6 without port, as you need to use bracket with port in IPV6
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type params struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseAddr(t *testing.T) {
|
func TestParseAddr(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -33,3 +31,36 @@ func TestParseAddr(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsurePort(t *testing.T) {
|
||||||
|
testAddresses := []string{
|
||||||
|
"1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad",
|
||||||
|
"1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad:5252",
|
||||||
|
"[::1]",
|
||||||
|
"127.0.0.1:5555",
|
||||||
|
"127.0.0.1",
|
||||||
|
"[::1]:5555",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, oldAddr := range testAddresses {
|
||||||
|
t.Run(oldAddr, func(st *testing.T) {
|
||||||
|
newAddr := ensurePort(oldAddr, 5222)
|
||||||
|
|
||||||
|
if len(newAddr) < len(oldAddr) {
|
||||||
|
st.Errorf("incorrect Result: transformed address is shorter than input : %v (old) > %v (new)", newAddr, oldAddr)
|
||||||
|
}
|
||||||
|
// If IPv6, the new address needs brackets to specify a port, like so : [2001:db8:85a3:0:0:8a2e:370:7334]:5222
|
||||||
|
if strings.Count(newAddr, "[") < strings.Count(oldAddr, "[") ||
|
||||||
|
strings.Count(newAddr, "]") < strings.Count(oldAddr, "]") {
|
||||||
|
|
||||||
|
st.Errorf("incorrect Result. Transformed address seems to not have correct brakets : %v => %v", oldAddr, newAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we messed up the colons, or didn't properly add a port
|
||||||
|
if strings.Count(newAddr, ":") < strings.Count(oldAddr, ":") {
|
||||||
|
st.Errorf("incorrect Result: transformed address doesn't seem to have a port %v (=> %v, no port ?)", oldAddr, newAddr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
50
router.go
50
router.go
|
@ -42,7 +42,18 @@ func NewRouter() *Router {
|
||||||
// route is called by the XMPP client to dispatch stanza received using the set up routes.
|
// 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.
|
// 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) {
|
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 {
|
if isIq {
|
||||||
r.IQResultRouteLock.RLock()
|
r.IQResultRouteLock.RLock()
|
||||||
route, ok := r.IQResultRoutes[iq.Id]
|
route, ok := r.IQResultRoutes[iq.Id]
|
||||||
|
@ -51,7 +62,7 @@ func (r *Router) route(s Sender, p stanza.Packet) {
|
||||||
r.IQResultRouteLock.Lock()
|
r.IQResultRouteLock.Lock()
|
||||||
delete(r.IQResultRoutes, iq.Id)
|
delete(r.IQResultRoutes, iq.Id)
|
||||||
r.IQResultRouteLock.Unlock()
|
r.IQResultRouteLock.Unlock()
|
||||||
route.result <- iq
|
route.result <- *iq
|
||||||
close(route.result)
|
close(route.result)
|
||||||
return
|
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{
|
err := stanza.Err{
|
||||||
XMLName: xml.Name{Local: "error"},
|
XMLName: xml.Name{Local: "error"},
|
||||||
Code: 501,
|
Code: 501,
|
||||||
|
@ -232,7 +270,7 @@ func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
switch p.(type) {
|
switch p.(type) {
|
||||||
case stanza.Message:
|
case stanza.Message:
|
||||||
name = "message"
|
name = "message"
|
||||||
case stanza.IQ:
|
case *stanza.IQ:
|
||||||
name = "iq"
|
name = "iq"
|
||||||
case stanza.Presence:
|
case stanza.Presence:
|
||||||
name = "presence"
|
name = "presence"
|
||||||
|
@ -259,7 +297,7 @@ type nsTypeMatcher []string
|
||||||
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
var stanzaType stanza.StanzaType
|
var stanzaType stanza.StanzaType
|
||||||
switch packet := p.(type) {
|
switch packet := p.(type) {
|
||||||
case stanza.IQ:
|
case *stanza.IQ:
|
||||||
stanzaType = packet.Type
|
stanzaType = packet.Type
|
||||||
case stanza.Presence:
|
case stanza.Presence:
|
||||||
stanzaType = packet.Type
|
stanzaType = packet.Type
|
||||||
|
@ -291,7 +329,7 @@ func (r *Route) StanzaType(types ...string) *Route {
|
||||||
type nsIQMatcher []string
|
type nsIQMatcher []string
|
||||||
|
|
||||||
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(*stanza.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,10 @@ func TestIQResultRoutes(t *testing.T) {
|
||||||
// Check if the IQ handler was called
|
// Check if the IQ handler was called
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||||
defer cancel()
|
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")
|
res := router.NewIQResultRoute(ctx, "1234")
|
||||||
go router.route(conn, iq)
|
go router.route(conn, iq)
|
||||||
select {
|
select {
|
||||||
|
@ -71,7 +74,10 @@ func TestNameMatcher(t *testing.T) {
|
||||||
|
|
||||||
// Check that an IQ packet is not matched
|
// Check that an IQ packet is not matched
|
||||||
conn = NewSenderMock()
|
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{}
|
iq.Payload = &stanza.DiscoInfo{}
|
||||||
router.route(conn, iq)
|
router.route(conn, iq)
|
||||||
if conn.String() == successFlag {
|
if conn.String() == successFlag {
|
||||||
|
@ -89,7 +95,10 @@ func TestIQNSMatcher(t *testing.T) {
|
||||||
|
|
||||||
// Check that an IQ with proper namespace does match
|
// Check that an IQ with proper namespace does match
|
||||||
conn := NewSenderMock()
|
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
|
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||||
iqDisco.Payload = &stanza.DiscoInfo{
|
iqDisco.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
|
@ -103,7 +112,10 @@ func TestIQNSMatcher(t *testing.T) {
|
||||||
|
|
||||||
// Check that another namespace is not matched
|
// Check that another namespace is not matched
|
||||||
conn = NewSenderMock()
|
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
|
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||||
iqVersion.Payload = &stanza.DiscoInfo{
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
|
@ -146,7 +158,10 @@ func TestTypeMatcher(t *testing.T) {
|
||||||
|
|
||||||
// We do not match on other types
|
// We do not match on other types
|
||||||
conn = NewSenderMock()
|
conn = NewSenderMock()
|
||||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", 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{
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
|
@ -163,28 +178,37 @@ func TestCompositeMatcher(t *testing.T) {
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
IQNamespaces("jabber:iq:version").
|
IQNamespaces("jabber:iq:version").
|
||||||
StanzaType("get").
|
StanzaType(string(stanza.IQTypeGet)).
|
||||||
HandlerFunc(func(s Sender, p stanza.Packet) {
|
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||||
_ = s.SendRaw(successFlag)
|
_ = s.SendRaw(successFlag)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Data set
|
// Data set
|
||||||
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", 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{
|
getVersionIq.Payload = &stanza.Version{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
Local: "query",
|
Local: "query",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", 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{
|
setVersionIq.Payload = &stanza.Version{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
Local: "query",
|
Local: "query",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
getDiscoIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
GetDiscoIq.Payload = &stanza.DiscoInfo{
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create getDiscoIq: %v", err)
|
||||||
|
}
|
||||||
|
getDiscoIq.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "http://jabber.org/protocol/disco#info",
|
Space: "http://jabber.org/protocol/disco#info",
|
||||||
Local: "query",
|
Local: "query",
|
||||||
|
@ -200,7 +224,7 @@ func TestCompositeMatcher(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{name: "match get version iq", input: getVersionIq, want: true},
|
{name: "match get version iq", input: getVersionIq, want: true},
|
||||||
{name: "ignore set version iq", input: setVersionIq, want: false},
|
{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},
|
{name: "ignore message", input: message, want: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +262,10 @@ func TestCatchallMatcher(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
conn = NewSenderMock()
|
conn = NewSenderMock()
|
||||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", 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{
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
|
@ -274,7 +301,7 @@ func (s SenderMock) Send(packet stanza.Packet) error {
|
||||||
return nil
|
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)
|
out, err := xml.Marshal(iq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
185
session.go
185
session.go
|
@ -1,10 +1,11 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
@ -23,44 +24,67 @@ type Session struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSession(transport Transport, o Config, state SMState) (*Session, error) {
|
func NewSession(c *Client, state SMState) (*Session, error) {
|
||||||
s := new(Session)
|
var s *Session
|
||||||
s.transport = transport
|
if c.Session == nil {
|
||||||
s.SMState = state
|
s = new(Session)
|
||||||
s.init(o)
|
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 {
|
if s.err != nil {
|
||||||
return nil, NewConnError(s.err, true)
|
return nil, NewConnError(s.err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !transport.IsSecure() {
|
if !c.transport.IsSecure() {
|
||||||
s.startTlsIfSupported(o)
|
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)
|
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
||||||
return nil, NewConnError(err, true)
|
return nil, NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.TlsEnabled {
|
if s.TlsEnabled {
|
||||||
s.reset(o)
|
s.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
s.auth(o)
|
s.auth(c.config)
|
||||||
s.reset(o)
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
s.reset()
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
// attempt resumption
|
// attempt resumption
|
||||||
if s.resume(o) {
|
if s.resume(c.config) {
|
||||||
return s, s.err
|
return s, s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, bind resource and 'start' XMPP session
|
// otherwise, bind resource and 'start' XMPP session
|
||||||
s.bind(o)
|
s.bind(c.config)
|
||||||
s.rfc3921Session(o)
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
s.rfc3921Session()
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
// Enable stream management if supported
|
// Enable stream management if supported
|
||||||
s.EnableStreamManagement(o)
|
s.EnableStreamManagement(c.config)
|
||||||
|
if s.err != nil {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
return s, s.err
|
return s, s.err
|
||||||
}
|
}
|
||||||
|
@ -70,19 +94,20 @@ func (s *Session) PacketId() string {
|
||||||
return fmt.Sprintf("%x", s.lastPacketId)
|
return fmt.Sprintf("%x", s.lastPacketId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) init(o Config) {
|
// init gathers information on the session such as stream features from the server.
|
||||||
s.Features = s.open(o.parsedJid.Domain)
|
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 {
|
if s.StreamId, s.err = s.transport.StartStream(); s.err != nil {
|
||||||
return
|
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
|
// extract stream features
|
||||||
if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil {
|
if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil {
|
||||||
s.err = errors.New("stream open decode features: " + s.err.Error())
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) startTlsIfSupported(o Config) {
|
func (s *Session) startTlsIfSupported(o *Config) {
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.transport.DoesStartTLS() {
|
if !s.transport.DoesStartTLS() {
|
||||||
if !o.Insecure {
|
if !o.Insecure {
|
||||||
s.err = errors.New("Transport does not support starttls")
|
s.err = errors.New("transport does not support starttls")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -119,13 +144,13 @@ func (s *Session) startTlsIfSupported(o Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we do not allow cleartext connections, make it explicit that server do not support starttls
|
// If we do not allow cleartext serverConnections, make it explicit that server do not support starttls
|
||||||
if !o.Insecure {
|
if !o.Insecure {
|
||||||
s.err = errors.New("XMPP server does not advertise support for starttls")
|
s.err = errors.New("XMPP server does not advertise support for starttls")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) auth(o Config) {
|
func (s *Session) auth(o *Config) {
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -134,7 +159,7 @@ func (s *Session) auth(o Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to resume session using stream management
|
// 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() {
|
if !s.Features.DoesStreamManagement() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -142,9 +167,16 @@ func (s *Session) resume(o Config) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(s.transport, "<resume xmlns='%s' h='%d' previd='%s'/>",
|
rsm := stanza.SMResume{
|
||||||
stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id)
|
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
|
var packet stanza.Packet
|
||||||
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||||
if s.err == nil {
|
if s.err == nil {
|
||||||
|
@ -165,20 +197,48 @@ func (s *Session) resume(o Config) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) bind(o Config) {
|
func (s *Session) bind(o *Config) {
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send IQ message asking to bind to the local user name.
|
// Send IQ message asking to bind to the local user name.
|
||||||
var resource = o.parsedJid.Resource
|
var resource = o.parsedJid.Resource
|
||||||
if resource != "" {
|
iqB, err := stanza.NewIQ(stanza.Attrs{
|
||||||
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
Type: stanza.IQTypeSet,
|
||||||
s.PacketId(), stanza.NSBind, resource)
|
Id: s.PacketId(),
|
||||||
} else {
|
})
|
||||||
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
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
|
var iq stanza.IQ
|
||||||
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||||
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
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.
|
// 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 {
|
if s.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -205,7 +265,29 @@ func (s *Session) rfc3921Session(o Config) {
|
||||||
var iq stanza.IQ
|
var iq stanza.IQ
|
||||||
// We only negotiate session binding if it is mandatory, we skip it when optional.
|
// We only negotiate session binding if it is mandatory, we skip it when optional.
|
||||||
if !s.Features.Session.IsOptional() {
|
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 {
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||||
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
||||||
return
|
return
|
||||||
|
@ -214,28 +296,47 @@ func (s *Session) rfc3921Session(o Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable stream management, with session resumption, if supported.
|
// Enable stream management, with session resumption, if supported.
|
||||||
func (s *Session) EnableStreamManagement(o Config) {
|
func (s *Session) EnableStreamManagement(o *Config) {
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(s.transport, "<enable xmlns='%s' resume='true'/>", stanza.NSStreamManagement)
|
|
||||||
|
|
||||||
var packet stanza.Packet
|
var packet stanza.Packet
|
||||||
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||||
if s.err == nil {
|
if s.err == nil {
|
||||||
switch p := packet.(type) {
|
switch p := packet.(type) {
|
||||||
case stanza.SMEnabled:
|
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:
|
case stanza.SMFailed:
|
||||||
// TODO: Store error in SMState, for later inspection
|
// 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:
|
default:
|
||||||
s.err = errors.New("unexpected reply to SM enable")
|
s.err = errors.New("unexpected reply to SM enable")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
153
stanza/commands.go
Normal file
153
stanza/commands.go
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// Implements the XEP-0050 extension
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommandActionCancel = "cancel"
|
||||||
|
CommandActionComplete = "complete"
|
||||||
|
CommandActionExecute = "execute"
|
||||||
|
CommandActionNext = "next"
|
||||||
|
CommandActionPrevious = "prev"
|
||||||
|
|
||||||
|
CommandStatusCancelled = "canceled"
|
||||||
|
CommandStatusCompleted = "completed"
|
||||||
|
CommandStatusExecuting = "executing"
|
||||||
|
|
||||||
|
CommandNoteTypeErr = "error"
|
||||||
|
CommandNoteTypeInfo = "info"
|
||||||
|
CommandNoteTypeWarn = "warn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"`
|
||||||
|
|
||||||
|
CommandElement CommandElement
|
||||||
|
|
||||||
|
BadAction *struct{} `xml:"bad-action,omitempty"`
|
||||||
|
BadLocale *struct{} `xml:"bad-locale,omitempty"`
|
||||||
|
BadPayload *struct{} `xml:"bad-payload,omitempty"`
|
||||||
|
BadSessionId *struct{} `xml:"bad-sessionid,omitempty"`
|
||||||
|
MalformedAction *struct{} `xml:"malformed-action,omitempty"`
|
||||||
|
SessionExpired *struct{} `xml:"session-expired,omitempty"`
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
Action string `xml:"action,attr,omitempty"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type Actions struct {
|
||||||
|
Prev *struct{} `xml:"prev,omitempty"`
|
||||||
|
Next *struct{} `xml:"next,omitempty"`
|
||||||
|
Complete *struct{} `xml:"complete,omitempty"`
|
||||||
|
|
||||||
|
Execute string `xml:"execute,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Actions) Ref() string {
|
||||||
|
return "actions"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Note struct {
|
||||||
|
Text string `xml:",cdata"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Note) Ref() string {
|
||||||
|
return "note"
|
||||||
|
}
|
||||||
|
func (f *Form) Ref() string { return "form" }
|
||||||
|
|
||||||
|
func (n *Node) Ref() string {
|
||||||
|
return "node"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
c.XMLName = start.Name
|
||||||
|
|
||||||
|
// Extract packet attributes
|
||||||
|
for _, attr := range start.Attr {
|
||||||
|
if attr.Name.Local == "action" {
|
||||||
|
c.Action = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "node" {
|
||||||
|
c.Node = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "sessionid" {
|
||||||
|
c.SessionId = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "status" {
|
||||||
|
c.Status = attr.Value
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "lang" {
|
||||||
|
c.Lang = attr.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 "affiliations":
|
||||||
|
a := Actions{}
|
||||||
|
err = d.DecodeElement(&a, &tt)
|
||||||
|
c.CommandElement = &a
|
||||||
|
case "configure":
|
||||||
|
nt := Note{}
|
||||||
|
err = d.DecodeElement(&nt, &tt)
|
||||||
|
c.CommandElement = &nt
|
||||||
|
case "x":
|
||||||
|
f := Form{}
|
||||||
|
err = d.DecodeElement(&f, &tt)
|
||||||
|
c.CommandElement = &f
|
||||||
|
default:
|
||||||
|
n := Node{}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/commands", Local: "command"}, Command{})
|
||||||
|
}
|
40
stanza/commands_test.go
Normal file
40
stanza/commands_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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>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)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal initial input")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal unmarshalled input")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := compareMarshal(input, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import (
|
||||||
type Handshake struct {
|
type Handshake struct {
|
||||||
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||||
// TODO Add handshake value with test for proper serialization
|
// TODO Add handshake value with test for proper serialization
|
||||||
// Value string `xml:",innerxml"`
|
Value string `xml:",innerxml"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Handshake) Name() string {
|
func (Handshake) Name() string {
|
||||||
|
@ -42,11 +42,16 @@ type Delegation struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
|
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
|
||||||
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
|
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
|
||||||
Delegated *Delegated // This is used in a message to confirm delegated namespace
|
Delegated *Delegated // This is used in a message to confirm delegated namespace
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Delegation) Namespace() string {
|
func (d *Delegation) Namespace() string {
|
||||||
return d.XMLName.Space
|
return d.XMLName.Space
|
||||||
}
|
}
|
||||||
|
func (d *Delegation) GetSet() *ResultSet {
|
||||||
|
return d.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
// Forwarded is used to wrapped forwarded stanzas.
|
// Forwarded is used to wrapped forwarded stanzas.
|
||||||
// TODO: Move it in another file, as it is not limited to components.
|
// TODO: Move it in another file, as it is not limited to components.
|
||||||
|
@ -86,6 +91,6 @@ type Delegated struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTMessage, 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{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,13 +61,13 @@ func TestParsingDelegationIQ(t *testing.T) {
|
||||||
if iq.Payload != nil {
|
if iq.Payload != nil {
|
||||||
if delegation, ok := iq.Payload.(*Delegation); ok {
|
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||||
packet := delegation.Forwarded.Stanza
|
packet := delegation.Forwarded.Stanza
|
||||||
forwardedIQ, ok := packet.(IQ)
|
forwardedIQ, ok := packet.(*IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Could not extract packet IQ")
|
t.Errorf("Could not extract packet IQ")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if forwardedIQ.Payload != nil {
|
if forwardedIQ.Payload != nil {
|
||||||
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
|
if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); ok {
|
||||||
node = pubsub.Publish.Node
|
node = pubsub.Publish.Node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
70
stanza/datetime_profiles.go
Normal file
70
stanza/datetime_profiles.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper structures and functions to manage dates and timestamps as defined in
|
||||||
|
// XEP-0082: XMPP Date and Time Profiles (https://xmpp.org/extensions/xep-0082.html)
|
||||||
|
|
||||||
|
const dateLayoutXEP0082 = "2006-01-02"
|
||||||
|
const timeLayoutXEP0082 = "15:04:05+00:00"
|
||||||
|
|
||||||
|
var InvalidDateInput = errors.New("could not parse date. Input might not be in a supported format")
|
||||||
|
var InvalidDateOutput = errors.New("could not format date as desired")
|
||||||
|
|
||||||
|
type JabberDate struct {
|
||||||
|
value time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d JabberDate) DateToString() string {
|
||||||
|
return d.value.Format(dateLayoutXEP0082)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d JabberDate) DateTimeToString(nanos bool) string {
|
||||||
|
if nanos {
|
||||||
|
return d.value.Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
return d.value.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d JabberDate) TimeToString(nanos bool) (string, error) {
|
||||||
|
if nanos {
|
||||||
|
spl := strings.Split(d.value.Format(time.RFC3339Nano), "T")
|
||||||
|
if len(spl) != 2 {
|
||||||
|
return "", InvalidDateOutput
|
||||||
|
}
|
||||||
|
return spl[1], nil
|
||||||
|
}
|
||||||
|
spl := strings.Split(d.value.Format(time.RFC3339), "T")
|
||||||
|
if len(spl) != 2 {
|
||||||
|
return "", InvalidDateOutput
|
||||||
|
}
|
||||||
|
return spl[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJabberDateFromString(strDate string) (JabberDate, error) {
|
||||||
|
t, err := time.Parse(time.RFC3339, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse(time.RFC3339Nano, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse(dateLayoutXEP0082, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse(timeLayoutXEP0082, strDate)
|
||||||
|
if err == nil {
|
||||||
|
return JabberDate{value: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return JabberDate{}, InvalidDateInput
|
||||||
|
}
|
191
stanza/datetime_profiles_test.go
Normal file
191
stanza/datetime_profiles_test.go
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDateToString(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
t2 := JabberDate{value: time.Now().Add(24 * time.Hour)}
|
||||||
|
|
||||||
|
t1Str := t1.DateToString()
|
||||||
|
t2Str := t2.DateToString()
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateToStringOracle(t *testing.T) {
|
||||||
|
expected := "2009-11-10"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str := t1.DateToString()
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToString(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(false)
|
||||||
|
t2Str := t2.DateTimeToString(false)
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToStringOracle(t *testing.T) {
|
||||||
|
expected := "2009-11-10T23:03:22+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(false)
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToStringNanos(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
time.After(10 * time.Millisecond)
|
||||||
|
t2 := JabberDate{value: time.Now()}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(true)
|
||||||
|
t2Str := t2.DateTimeToString(true)
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToStringNanosOracle(t *testing.T) {
|
||||||
|
expected := "2009-11-10T23:03:22.000000089+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str := t1.DateTimeToString(true)
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeToString(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t2Str, err := t2.TimeToString(false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeToStringOracle(t *testing.T) {
|
||||||
|
expected := "23:03:22+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeToStringNanos(t *testing.T) {
|
||||||
|
t1 := JabberDate{value: time.Now()}
|
||||||
|
time.After(10 * time.Millisecond)
|
||||||
|
t2 := JabberDate{value: time.Now()}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t2Str, err := t2.TimeToString(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str == t2Str {
|
||||||
|
t.Fatalf("time representations should not be identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestTimeToStringNanosOracle(t *testing.T) {
|
||||||
|
expected := "23:03:22.000000089+08:00"
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||||
|
|
||||||
|
t1Str, err := t1.TimeToString(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t1Str != expected {
|
||||||
|
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJabberDateParsing(t *testing.T) {
|
||||||
|
date := "2009-11-10"
|
||||||
|
_, err := NewJabberDateFromString(date)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTime := "2009-11-10T23:03:22+08:00"
|
||||||
|
_, err = NewJabberDateFromString(dateTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTimeNanos := "2009-11-10T23:03:22.000000089+08:00"
|
||||||
|
_, err = NewJabberDateFromString(dateTimeNanos)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : fix these. Parsing a time with an offset doesn't work
|
||||||
|
//time := "23:03:22+08:00"
|
||||||
|
//_, err = NewJabberDateFromString(time)
|
||||||
|
//if err != nil {
|
||||||
|
// t.Fatalf(err.Error())
|
||||||
|
//}
|
||||||
|
|
||||||
|
//timeNanos := "23:03:22.000000089+08:00"
|
||||||
|
//_, err = NewJabberDateFromString(timeNanos)
|
||||||
|
//if err != nil {
|
||||||
|
// t.Fatalf(err.Error())
|
||||||
|
//}
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package stanza
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
@ -53,10 +54,19 @@ func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||||
if elt.XMLName == textName {
|
// TODO : change the pubsub handling ? It kind of dilutes the information
|
||||||
x.Text = string(elt.Content)
|
// Handles : 6.1.3.11 Node Has Moved for XEP-0060 (PubSubGeneric)
|
||||||
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
|
goneName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "gone"}
|
||||||
x.Reason = elt.XMLName.Local
|
if elt.XMLName == textName || // Regular error text
|
||||||
|
elt.XMLName == goneName { // Gone text for pubsub
|
||||||
|
x.Text = elt.Content
|
||||||
|
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" ||
|
||||||
|
elt.XMLName.Space == "http://jabber.org/protocol/pubsub#errors" {
|
||||||
|
if strings.TrimSpace(x.Reason) != "" {
|
||||||
|
x.Reason = strings.Join([]string{elt.XMLName.Local}, ":")
|
||||||
|
} else {
|
||||||
|
x.Reason = elt.XMLName.Local
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case xml.EndElement:
|
case xml.EndElement:
|
||||||
|
@ -94,16 +104,32 @@ func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||||
// Reason
|
// Reason
|
||||||
if x.Reason != "" {
|
if x.Reason != "" {
|
||||||
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
|
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
|
||||||
e.EncodeToken(xml.StartElement{Name: reason})
|
err = e.EncodeToken(xml.StartElement{Name: reason})
|
||||||
e.EncodeToken(xml.EndElement{Name: reason})
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.EndElement{Name: reason})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
if x.Text != "" {
|
if x.Text != "" {
|
||||||
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||||
e.EncodeToken(xml.StartElement{Name: text})
|
err = e.EncodeToken(xml.StartElement{Name: text})
|
||||||
e.EncodeToken(xml.CharData(x.Text))
|
if err != nil {
|
||||||
e.EncodeToken(xml.EndElement{Name: text})
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.CharData(x.Text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.EndElement{Name: text})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||||
|
|
34
stanza/fifo_queue.go
Normal file
34
stanza/fifo_queue.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
// FIFO queue for string contents
|
||||||
|
// Implementations have no guarantee regarding thread safety !
|
||||||
|
type FifoQueue interface {
|
||||||
|
// Pop returns the first inserted element still in queue and deletes it from queue. If queue is empty, returns nil
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Pop() Queueable
|
||||||
|
|
||||||
|
// PopN returns the N first inserted elements still in queue and deletes them from queue. If queue is empty or i<=0, returns nil
|
||||||
|
// If number to pop is greater than queue length, returns all queue elements
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
PopN(i int) []Queueable
|
||||||
|
|
||||||
|
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty, returns nil
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Peek() Queueable
|
||||||
|
|
||||||
|
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty or i<=0, returns nil.
|
||||||
|
// If number to peek is greater than queue length, returns all queue elements
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
PeekN() []Queueable
|
||||||
|
// Push adds an element to the queue
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Push(s Queueable) error
|
||||||
|
|
||||||
|
// Empty returns true if queue is empty
|
||||||
|
// No guarantee regarding thread safety !
|
||||||
|
Empty() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queueable interface {
|
||||||
|
QueueableName() string
|
||||||
|
}
|
68
stanza/form.go
Normal file
68
stanza/form.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
type FormType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormTypeCancel = "cancel"
|
||||||
|
FormTypeForm = "form"
|
||||||
|
FormTypeResult = "result"
|
||||||
|
FormTypeSubmit = "submit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 `xml:"item,omitempty"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItem struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Fields []Field `xml:"field,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Field struct {
|
||||||
|
XMLName xml.Name `xml:"field"`
|
||||||
|
Description string `xml:"desc,omitempty"`
|
||||||
|
Required *string `xml:"required"`
|
||||||
|
ValuesList []string `xml:"value"`
|
||||||
|
Options []Option `xml:"option,omitempty"`
|
||||||
|
Var string `xml:"var,attr,omitempty"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
Label string `xml:"label,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewForm(fields []*Field, formType string) *Form {
|
||||||
|
return &Form{
|
||||||
|
Type: formType,
|
||||||
|
Fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldTypeBool = "boolean"
|
||||||
|
FieldTypeFixed = "fixed"
|
||||||
|
FieldTypeHidden = "hidden"
|
||||||
|
FieldTypeJidMulti = "jid-multi"
|
||||||
|
FieldTypeJidSingle = "jid-single"
|
||||||
|
FieldTypeListMulti = "list-multi"
|
||||||
|
FieldTypeListSingle = "list-single"
|
||||||
|
FieldTypeTextMulti = "text-multi"
|
||||||
|
FieldTypeTextPrivate = "text-private"
|
||||||
|
FieldTypeTextSingle = "text-Single"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
XMLName xml.Name `xml:"option"`
|
||||||
|
Label string `xml:"label,attr,omitempty"`
|
||||||
|
ValuesList []string `xml:"value"`
|
||||||
|
}
|
110
stanza/form_test.go
Normal file
110
stanza/form_test.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
formSubmit = "<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#title\">" +
|
||||||
|
"<value>Princely Musings (Atom)</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#deliver_notifications\">" +
|
||||||
|
"<value>1</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>" +
|
||||||
|
"<field var=\"pubsub#type\">" +
|
||||||
|
"<value>http://www.w3.org/2005/Atom</value>" +
|
||||||
|
"</field>" +
|
||||||
|
"<field var=\"pubsub#notification_type\" type=\"list-single\"" +
|
||||||
|
"label=\"Specify the delivery style for event notifications\">" +
|
||||||
|
"<value>headline</value>" +
|
||||||
|
"<option>" +
|
||||||
|
"<value>normal</value>" +
|
||||||
|
"</option>" +
|
||||||
|
"<option>" +
|
||||||
|
"<value>headline</value>" +
|
||||||
|
"</option>" +
|
||||||
|
"</field>" +
|
||||||
|
"</x>" +
|
||||||
|
"</configure>" +
|
||||||
|
"</pubsub>"
|
||||||
|
|
||||||
|
clientJid = "hamlet@denmark.lit/elsinore"
|
||||||
|
serviceJid = "pubsub.shakespeare.lit"
|
||||||
|
iqId = "config1"
|
||||||
|
serviceNode = "princely_musings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalFormSubmit(t *testing.T) {
|
||||||
|
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{
|
||||||
|
{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"}},
|
||||||
|
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
|
||||||
|
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
|
||||||
|
{Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}},
|
||||||
|
{
|
||||||
|
Var: "pubsub#notification_type",
|
||||||
|
Type: "list-single",
|
||||||
|
Label: "Specify the delivery style for event notifications",
|
||||||
|
ValuesList: []string{"headline"},
|
||||||
|
Options: []Option{
|
||||||
|
{ValuesList: []string{"normal"}},
|
||||||
|
{ValuesList: []string{"headline"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := xml.Marshal(formIQ.Payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not marshal formIQ : %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ReplaceAll(string(b), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
|
||||||
|
t.Fatalf("Expected formIQ and marshalled one are different.\nExepected : %s\nMarshalled : %s", formSubmit, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalFormSubmit(t *testing.T) {
|
||||||
|
var f PubSubOwner
|
||||||
|
mErr := xml.Unmarshal([]byte(formSubmit), &f)
|
||||||
|
if mErr != nil {
|
||||||
|
t.Fatalf("failed to unmarshal formSubmit ! %s", mErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(&f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal formSubmit")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ReplaceAll(string(data), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
|
||||||
|
t.Fatalf("failed unmarshal/marshal for formSubmit : %s\n%s", string(data), formSubmit)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,12 +7,18 @@ import (
|
||||||
type ControlSet struct {
|
type ControlSet struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
|
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
|
||||||
Fields []ControlField `xml:",any"`
|
Fields []ControlField `xml:",any"`
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlSet) Namespace() string {
|
func (c *ControlSet) Namespace() string {
|
||||||
return c.XMLName.Space
|
return c.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ControlSet) GetSet() *ResultSet {
|
||||||
|
return c.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
type ControlGetForm struct {
|
type ControlGetForm struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
|
XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
|
||||||
}
|
}
|
||||||
|
@ -30,10 +36,13 @@ type ControlSetResponse struct {
|
||||||
func (c *ControlSetResponse) Namespace() string {
|
func (c *ControlSetResponse) Namespace() string {
|
||||||
return c.XMLName.Space
|
return c.XMLName.Space
|
||||||
}
|
}
|
||||||
|
func (c *ControlSetResponse) GetSet() *ResultSet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Registry init
|
// Registry init
|
||||||
|
|
||||||
func 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{})
|
||||||
}
|
}
|
||||||
|
|
78
stanza/iq.go
78
stanza/iq.go
|
@ -2,6 +2,8 @@ package stanza
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
@ -23,52 +25,63 @@ type IQ struct { // Info/Query
|
||||||
// child element, which specifies the semantics of the particular
|
// child element, which specifies the semantics of the particular
|
||||||
// request."
|
// request."
|
||||||
Payload IQPayload `xml:",omitempty"`
|
Payload IQPayload `xml:",omitempty"`
|
||||||
Error Err `xml:"error,omitempty"`
|
Error *Err `xml:"error,omitempty"`
|
||||||
// Any is used to decode unknown payload as a generic structure
|
// Any is used to decode unknown payload as a generic structure
|
||||||
Any *Node `xml:",any"`
|
Any *Node `xml:",any"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IQPayload interface {
|
type IQPayload interface {
|
||||||
Namespace() string
|
Namespace() string
|
||||||
|
GetSet() *ResultSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIQ(a Attrs) IQ {
|
func NewIQ(a Attrs) (*IQ, error) {
|
||||||
// TODO ensure that type is set, as it is required
|
|
||||||
if a.Id == "" {
|
if a.Id == "" {
|
||||||
if id, err := uuid.NewRandom(); err == nil {
|
if id, err := uuid.NewRandom(); err == nil {
|
||||||
a.Id = id.String()
|
a.Id = id.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return IQ{
|
|
||||||
|
iq := IQ{
|
||||||
XMLName: xml.Name{Local: "iq"},
|
XMLName: xml.Name{Local: "iq"},
|
||||||
Attrs: a,
|
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
|
from := iq.From
|
||||||
to := iq.To
|
to := iq.To
|
||||||
|
|
||||||
iq.Type = "error"
|
iq.Type = "error"
|
||||||
iq.From = to
|
iq.From = to
|
||||||
iq.To = from
|
iq.To = from
|
||||||
iq.Error = xerror
|
iq.Error = &xerror
|
||||||
|
|
||||||
return iq
|
return iq
|
||||||
}
|
}
|
||||||
|
|
||||||
func (IQ) Name() string {
|
func (*IQ) Name() string {
|
||||||
return "iq"
|
return "iq"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NoOp to implement BiDirIteratorElt
|
||||||
|
func (*IQ) NoOp() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
type iqDecoder struct{}
|
type iqDecoder struct{}
|
||||||
|
|
||||||
var iq iqDecoder
|
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
|
var packet IQ
|
||||||
err := p.DecodeElement(&packet, &se)
|
err := p.DecodeElement(&packet, &se)
|
||||||
return packet, err
|
return &packet, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing for IQs
|
// UnmarshalXML implements custom parsing for IQs
|
||||||
|
@ -106,7 +119,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
iq.Error = xmppError
|
iq.Error = &xmppError
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
||||||
|
@ -132,3 +145,48 @@ 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, error) {
|
||||||
|
// ID is required
|
||||||
|
if len(strings.TrimSpace(iq.Id)) == 0 {
|
||||||
|
return false, IqIDUnset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type is required
|
||||||
|
if iq.Type.IsEmpty() {
|
||||||
|
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, IqSGetNoPl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A result must include zero or one child element
|
||||||
|
if iq.Type == IQTypeResult {
|
||||||
|
if iq.Payload != nil && iq.Any != nil {
|
||||||
|
return false, IqResNoPl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Error type must contain an "error" child element
|
||||||
|
if iq.Type == IQTypeError {
|
||||||
|
if iq.Error == nil {
|
||||||
|
return false, IqErrNoErrPl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
// Disco Info
|
// Disco Info
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// NSDiscoInfo defines the namespace for disco IQ stanzas
|
||||||
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,16 +16,22 @@ const (
|
||||||
// Namespaces
|
// Namespaces
|
||||||
|
|
||||||
type DiscoInfo struct {
|
type DiscoInfo struct {
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
|
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
|
||||||
Node string `xml:"node,attr,omitempty"`
|
Node string `xml:"node,attr,omitempty"`
|
||||||
Identity []Identity `xml:"identity"`
|
Identity []Identity `xml:"identity"`
|
||||||
Features []Feature `xml:"feature"`
|
Features []Feature `xml:"feature"`
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Namespace lets DiscoInfo implement the IQPayload interface
|
||||||
func (d *DiscoInfo) Namespace() string {
|
func (d *DiscoInfo) Namespace() string {
|
||||||
return d.XMLName.Space
|
return d.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DiscoInfo) GetSet() *ResultSet {
|
||||||
|
return d.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------
|
// ---------------
|
||||||
// Builder helpers
|
// Builder helpers
|
||||||
|
|
||||||
|
@ -100,19 +107,26 @@ type DiscoItems struct {
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||||
Node string `xml:"node,attr,omitempty"`
|
Node string `xml:"node,attr,omitempty"`
|
||||||
Items []DiscoItem `xml:"item"`
|
Items []DiscoItem `xml:"item"`
|
||||||
|
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscoItems) Namespace() string {
|
func (d *DiscoItems) Namespace() string {
|
||||||
return d.XMLName.Space
|
return d.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DiscoItems) GetSet() *ResultSet {
|
||||||
|
return d.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------
|
// ---------------
|
||||||
// Builder helpers
|
// Builder helpers
|
||||||
|
|
||||||
// DiscoItems builds a default DiscoItems payload
|
// DiscoItems builds a default DiscoItems payload
|
||||||
func (iq *IQ) DiscoItems() *DiscoItems {
|
func (iq *IQ) DiscoItems() *DiscoItems {
|
||||||
d := DiscoItems{
|
d := DiscoItems{
|
||||||
XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", Local: "query"},
|
XMLName: xml.Name{Space: NSDiscoItems, Local: "query"},
|
||||||
}
|
}
|
||||||
iq.Payload = &d
|
iq.Payload = &d
|
||||||
return &d
|
return &d
|
||||||
|
@ -144,6 +158,6 @@ type DiscoItem struct {
|
||||||
// Registry init
|
// Registry init
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoInfo, Local: "query"}, DiscoInfo{})
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoItems, Local: "query"}, DiscoItems{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,10 @@ import (
|
||||||
|
|
||||||
// Test DiscoInfo Builder with several features
|
// Test DiscoInfo Builder with several features
|
||||||
func TestDiscoInfo_Builder(t *testing.T) {
|
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 := iq.DiscoInfo()
|
||||||
disco.AddIdentity("Test Component", "gateway", "service")
|
disco.AddIdentity("Test Component", "gateway", "service")
|
||||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
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
|
// Implements XEP-0030 example 17
|
||||||
// https://xmpp.org/extensions/xep-0030.html#example-17
|
// https://xmpp.org/extensions/xep-0030.html#example-17
|
||||||
func TestDiscoItems_Builder(t *testing.T) {
|
func TestDiscoItems_Builder(t *testing.T) {
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: "result", 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"})
|
To: "romeo@montague.net/orchard", Id: "items-2"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
iq.DiscoItems().
|
iq.DiscoItems().
|
||||||
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
|
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
|
||||||
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
|
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
|
||||||
|
@ -73,11 +79,11 @@ func TestDiscoItems_Builder(t *testing.T) {
|
||||||
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
|
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
|
||||||
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
|
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
|
||||||
if len(pp.Items) != len(items) {
|
if len(pp.Items) != len(items) {
|
||||||
t.Errorf("Items length mismatch: %#v", pp.Items)
|
t.Errorf("List length mismatch: %#v", pp.Items)
|
||||||
} else {
|
} else {
|
||||||
for i, item := range pp.Items {
|
for i, item := range pp.Items {
|
||||||
if item.JID != items[i].JID {
|
if item.JID != items[i].JID {
|
||||||
t.Errorf("JID Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
}
|
}
|
||||||
if item.Node != items[i].Node {
|
if item.Node != items[i].Node {
|
||||||
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
|
|
126
stanza/iq_roster.go
Normal file
126
stanza/iq_roster.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Roster
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NSRoster is the Roster IQ namespace
|
||||||
|
NSRoster = "jabber:iq:roster"
|
||||||
|
// SubscriptionNone indicates the user does not have a subscription to
|
||||||
|
// the contact's presence, and the contact does not have a subscription
|
||||||
|
// to the user's presence; this is the default value, so if the subscription
|
||||||
|
// attribute is not included then the state is to be understood as "none"
|
||||||
|
SubscriptionNone = "none"
|
||||||
|
|
||||||
|
// SubscriptionTo indicates the user has a subscription to the contact's
|
||||||
|
// presence, but the contact does not have a subscription to the user's presence.
|
||||||
|
SubscriptionTo = "to"
|
||||||
|
|
||||||
|
// SubscriptionFrom indicates the contact has a subscription to the user's
|
||||||
|
// presence, but the user does not have a subscription to the contact's presence
|
||||||
|
SubscriptionFrom = "from"
|
||||||
|
|
||||||
|
// SubscriptionBoth indicates the user and the contact have subscriptions to each
|
||||||
|
// other's presence (also called a "mutual subscription")
|
||||||
|
SubscriptionBoth = "both"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------
|
||||||
|
// Namespaces
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// RosterIQ builds a default Roster payload
|
||||||
|
func (iq *IQ) RosterIQ() *Roster {
|
||||||
|
r := Roster{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: NSRoster,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iq.Payload = &r
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------
|
||||||
|
// SubElements
|
||||||
|
|
||||||
|
// RosterItems represents the list of items in a roster IQ
|
||||||
|
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
|
||||||
|
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"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
Ask string `xml:"ask,attr,omitempty"`
|
||||||
|
Name string `xml:"name,attr,omitempty"`
|
||||||
|
Subscription string `xml:"subscription,attr,omitempty"`
|
||||||
|
Groups []string `xml:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// RosterItems builds a default RosterItems payload
|
||||||
|
func (iq *IQ) RosterItems() *RosterItems {
|
||||||
|
ri := RosterItems{
|
||||||
|
XMLName: xml.Name{Space: "jabber:iq:roster", Local: "query"},
|
||||||
|
}
|
||||||
|
iq.Payload = &ri
|
||||||
|
return &ri
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddItem builds an item and ads it to the roster IQ
|
||||||
|
func (r *RosterItems) AddItem(jid, subscription, ask, name string, groups []string) *RosterItems {
|
||||||
|
item := RosterItem{
|
||||||
|
Jid: jid,
|
||||||
|
Name: name,
|
||||||
|
Groups: groups,
|
||||||
|
Subscription: subscription,
|
||||||
|
Ask: ask,
|
||||||
|
}
|
||||||
|
r.Items = append(r.Items, item)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Registry init
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, Roster{})
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, RosterItems{})
|
||||||
|
}
|
112
stanza/iq_roster_test.go
Normal file
112
stanza/iq_roster_test.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRosterBuilder(t *testing.T) {
|
||||||
|
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",
|
||||||
|
SubscriptionBoth,
|
||||||
|
"",
|
||||||
|
"xl8ceaw",
|
||||||
|
[]string{"0flucpm8i2jtrjhxw01uf1nd2",
|
||||||
|
"bm2bajg9ex4e1swiuju9i9nu5",
|
||||||
|
"rvjpanomi4ejpx42fpmffoac0"}).
|
||||||
|
AddItem("9aynsym60zbu78jbdvpho7s68@crypho.com",
|
||||||
|
SubscriptionBoth,
|
||||||
|
"",
|
||||||
|
"9aynsym60",
|
||||||
|
[]string{"mzaoy73i6ra5k502182zi1t97"}).
|
||||||
|
AddItem("admin@crypho.com",
|
||||||
|
SubscriptionBoth,
|
||||||
|
"",
|
||||||
|
"admin",
|
||||||
|
noGroup)
|
||||||
|
|
||||||
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
pp, ok := parsedIQ.Payload.(*RosterItems)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check items
|
||||||
|
items := []RosterItem{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Name: "xl8ceaw",
|
||||||
|
Ask: "",
|
||||||
|
Jid: "xl8ceawrfu8zdneomw1h6h28d@crypho.com",
|
||||||
|
Subscription: SubscriptionBoth,
|
||||||
|
Groups: []string{"0flucpm8i2jtrjhxw01uf1nd2",
|
||||||
|
"bm2bajg9ex4e1swiuju9i9nu5",
|
||||||
|
"rvjpanomi4ejpx42fpmffoac0"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Name: "9aynsym60",
|
||||||
|
Ask: "",
|
||||||
|
Jid: "9aynsym60zbu78jbdvpho7s68@crypho.com",
|
||||||
|
Subscription: SubscriptionBoth,
|
||||||
|
Groups: []string{"mzaoy73i6ra5k502182zi1t97"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Name: "admin",
|
||||||
|
Ask: "",
|
||||||
|
Jid: "admin@crypho.com",
|
||||||
|
Subscription: SubscriptionBoth,
|
||||||
|
Groups: noGroup,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if len(pp.Items) != len(items) {
|
||||||
|
t.Errorf("List length mismatch: %#v", pp.Items)
|
||||||
|
} else {
|
||||||
|
for i, item := range pp.Items {
|
||||||
|
if item.Jid != items[i].Jid {
|
||||||
|
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(item.Groups, items[i].Groups) {
|
||||||
|
t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if item.Name != items[i].Name {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if item.Ask != items[i].Ask {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
if item.Subscription != items[i].Subscription {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMarshalling(t *testing.T, iq *IQ) (*IQ, error) {
|
||||||
|
// Marshall
|
||||||
|
data, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshall
|
||||||
|
var parsedIQ IQ
|
||||||
|
err = xml.Unmarshal(data, &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
|
||||||
|
}
|
||||||
|
return &parsedIQ, err
|
||||||
|
}
|
|
@ -36,24 +36,36 @@ func TestUnmarshalIqs(t *testing.T) {
|
||||||
|
|
||||||
func TestGenerateIqId(t *testing.T) {
|
func TestGenerateIqId(t *testing.T) {
|
||||||
t.Parallel()
|
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" {
|
if iq.Id != "1" {
|
||||||
t.Errorf("NewIQ replaced id with %s", iq.Id)
|
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 == "" {
|
if iq.Id == "" {
|
||||||
t.Error("NewIQ did not generate an 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 {
|
if iq.Id == otherIq.Id {
|
||||||
t.Errorf("NewIQ generated two identical ids: %s", iq.Id)
|
t.Errorf("NewIQ generated two identical ids: %s", iq.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateIq(t *testing.T) {
|
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{
|
payload := stanza.DiscoInfo{
|
||||||
Identity: []stanza.Identity{
|
Identity: []stanza.Identity{
|
||||||
{Name: "Test Gateway",
|
{Name: "Test Gateway",
|
||||||
|
@ -111,7 +123,10 @@ func TestErrorTag(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiscoItems(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{
|
payload := stanza.DiscoItems{
|
||||||
Node: "music",
|
Node: "music",
|
||||||
}
|
}
|
||||||
|
@ -187,3 +202,39 @@ func TestUnknownPayload(t *testing.T) {
|
||||||
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
|
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsValid(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
iq string
|
||||||
|
shouldErr bool
|
||||||
|
}
|
||||||
|
testIQs := make(map[string]testCase)
|
||||||
|
testIQs["Valid IQ"] = testCase{
|
||||||
|
`<iq type="get" to="service.localhost" id="1" >
|
||||||
|
<query xmlns="unknown:ns"/>
|
||||||
|
</iq>`,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
testIQs["Invalid IQ"] = testCase{
|
||||||
|
`<iq type="get" to="service.localhost">
|
||||||
|
<query xmlns="unknown:ns"/>
|
||||||
|
</iq>`,
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tcase := range testIQs {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
err := xml.Unmarshal([]byte(tcase.iq), &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isValid, err := parsedIQ.IsValid()
|
||||||
|
if !isValid && !tcase.shouldErr {
|
||||||
|
t.Errorf("failed validation for iq because: %s\nin test case : %s", err, tcase.iq)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -11,12 +11,18 @@ type Version struct {
|
||||||
Name string `xml:"name,omitempty"`
|
Name string `xml:"name,omitempty"`
|
||||||
Version string `xml:"version,omitempty"`
|
Version string `xml:"version,omitempty"`
|
||||||
OS string `xml:"os,omitempty"`
|
OS string `xml:"os,omitempty"`
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Version) Namespace() string {
|
func (v *Version) Namespace() string {
|
||||||
return v.XMLName.Space
|
return v.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Version) GetSet() *ResultSet {
|
||||||
|
return v.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------
|
// ---------------
|
||||||
// Builder helpers
|
// Builder helpers
|
||||||
|
|
||||||
|
@ -41,5 +47,5 @@ func (v *Version) SetInfo(name, version, os string) *Version {
|
||||||
// Registry init
|
// Registry init
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "jabber:iq:version", Local: "query"}, Version{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,11 @@ func TestVersion_Builder(t *testing.T) {
|
||||||
name := "Exodus"
|
name := "Exodus"
|
||||||
version := "0.7.0.4"
|
version := "0.7.0.4"
|
||||||
os := "Windows-XP 5.01.2600"
|
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"})
|
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)
|
iq.Version().SetInfo(name, version, os)
|
||||||
|
|
||||||
parsedIQ, err := checkMarshalling(t, iq)
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package xmpp
|
package stanza
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -20,9 +20,9 @@ func NewJid(sjid string) (*Jid, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s1 := strings.SplitN(sjid, "@", 2)
|
s1 := strings.SplitN(sjid, "@", 2)
|
||||||
if len(s1) == 1 { // This is a server or component JID
|
if len(s1) == 1 { // This is a server or component Jid
|
||||||
jid.Domain = s1[0]
|
jid.Domain = s1[0]
|
||||||
} else { // JID has a local username part
|
} else { // Jid has a local username part
|
||||||
if s1[0] == "" {
|
if s1[0] == "" {
|
||||||
return jid, fmt.Errorf("invalid jid '%s", sjid)
|
return jid, fmt.Errorf("invalid jid '%s", sjid)
|
||||||
}
|
}
|
||||||
|
@ -41,10 +41,10 @@ func NewJid(sjid string) (*Jid, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isUsernameValid(jid.Node) {
|
if !isUsernameValid(jid.Node) {
|
||||||
return jid, fmt.Errorf("invalid Node in JID '%s'", sjid)
|
return jid, fmt.Errorf("invalid Node in Jid '%s'", sjid)
|
||||||
}
|
}
|
||||||
if !isDomainValid(jid.Domain) {
|
if !isDomainValid(jid.Domain) {
|
||||||
return jid, fmt.Errorf("invalid domain in JID '%s'", sjid)
|
return jid, fmt.Errorf("invalid domain in Jid '%s'", sjid)
|
||||||
}
|
}
|
||||||
|
|
||||||
return jid, nil
|
return jid, nil
|
|
@ -1,4 +1,4 @@
|
||||||
package xmpp
|
package stanza
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -35,8 +35,8 @@ type MarkAcknowledged struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "markable"}, Markable{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "markable"}, Markable{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "received"}, MarkReceived{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "received"}, MarkReceived{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "displayed"}, MarkDisplayed{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "displayed"}, MarkDisplayed{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "acknowledged"}, MarkAcknowledged{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "acknowledged"}, MarkAcknowledged{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,9 @@ type StatePaused struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "active"}, StateActive{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "active"}, StateActive{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "composing"}, StateComposing{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "composing"}, StateComposing{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "gone"}, StateGone{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "gone"}, StateGone{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "inactive"}, StateInactive{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "inactive"}, StateInactive{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "paused"}, StatePaused{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "paused"}, StatePaused{})
|
||||||
}
|
}
|
||||||
|
|
36
stanza/msg_hint.go
Normal file
36
stanza/msg_hint.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Support for:
|
||||||
|
- XEP-0334: Message Processing Hints: https://xmpp.org/extensions/xep-0334.html
|
||||||
|
Pointers should be used to keep consistent with unmarshal. Eg :
|
||||||
|
msg.Extensions = append(msg.Extensions, &stanza.HintNoCopy{}, &stanza.HintStore{})
|
||||||
|
*/
|
||||||
|
|
||||||
|
type HintNoPermanentStore struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints no-permanent-store"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HintNoStore struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints no-store"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HintNoCopy struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints no-copy"`
|
||||||
|
}
|
||||||
|
type HintStore struct {
|
||||||
|
MsgExtension
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:hints store"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-permanent-store"}, HintNoPermanentStore{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-store"}, HintNoStore{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-copy"}, HintNoCopy{})
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "store"}, HintStore{})
|
||||||
|
}
|
72
stanza/msg_hint_test.go
Normal file
72
stanza/msg_hint_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const msg_const = `
|
||||||
|
<message
|
||||||
|
from="romeo@montague.lit/laptop"
|
||||||
|
to="juliet@capulet.lit/laptop">
|
||||||
|
<body>V unir avtugf pybnx gb uvqr zr sebz gurve fvtug</body>
|
||||||
|
<no-copy xmlns="urn:xmpp:hints"></no-copy>
|
||||||
|
<no-permanent-store xmlns="urn:xmpp:hints"></no-permanent-store>
|
||||||
|
<no-store xmlns="urn:xmpp:hints"></no-store>
|
||||||
|
<store xmlns="urn:xmpp:hints"></store>
|
||||||
|
</message>`
|
||||||
|
|
||||||
|
func TestSerializationHint(t *testing.T) {
|
||||||
|
msg := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
|
||||||
|
msg.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
|
||||||
|
msg.Extensions = append(msg.Extensions, stanza.HintNoCopy{}, stanza.HintNoPermanentStore{}, stanza.HintNoStore{}, stanza.HintStore{})
|
||||||
|
data, _ := xml.Marshal(msg)
|
||||||
|
if strings.ReplaceAll(strings.Join(strings.Fields(msg_const), ""), "\n", "") != strings.Join(strings.Fields(string(data)), "") {
|
||||||
|
t.Fatalf("marshalled message does not match expected message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalHints(t *testing.T) {
|
||||||
|
// Init message as in the const value
|
||||||
|
msgConst := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
|
||||||
|
msgConst.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
|
||||||
|
msgConst.Extensions = append(msgConst.Extensions, &stanza.HintNoCopy{}, &stanza.HintNoPermanentStore{}, &stanza.HintNoStore{}, &stanza.HintStore{})
|
||||||
|
|
||||||
|
// Compare message with the const value
|
||||||
|
msg := stanza.Message{}
|
||||||
|
err := xml.Unmarshal([]byte(msg_const), &msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msgConst.XMLName.Local != msg.XMLName.Local {
|
||||||
|
t.Fatalf("message tags do not match. Expected: %s, Actual: %s", msgConst.XMLName.Local, msg.XMLName.Local)
|
||||||
|
}
|
||||||
|
if msgConst.Body != msg.Body {
|
||||||
|
t.Fatalf("message bodies do not match. Expected: %s, Actual: %s", msgConst.Body, msg.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(msgConst.Attrs, msg.Attrs) {
|
||||||
|
t.Fatalf("attributes do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(msgConst.Error, msg.Error) {
|
||||||
|
t.Fatalf("attributes do not match")
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, ext := range msgConst.Extensions {
|
||||||
|
for _, strExt := range msg.Extensions {
|
||||||
|
if reflect.TypeOf(ext) == reflect.TypeOf(strExt) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("extensions do not match")
|
||||||
|
}
|
||||||
|
found = false
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,5 +18,5 @@ type HTMLBody struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"http://jabber.org/protocol/xhtml-im", "html"}, HTML{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/xhtml-im", Local: "html"}, HTML{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,5 @@ type OOB struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"jabber:x:oob", "x"}, OOB{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "jabber:x:oob", Local: "x"}, OOB{})
|
||||||
}
|
}
|
||||||
|
|
213
stanza/msg_pubsub_event.go
Normal file
213
stanza/msg_pubsub_event.go
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implementation of the http://jabber.org/protocol/pubsub#event namespace
|
||||||
|
type PubSubEvent struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#event event"`
|
||||||
|
MsgExtension
|
||||||
|
EventElement EventElement
|
||||||
|
//List ItemsEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/pubsub#event", Local: "event"}, PubSubEvent{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventElement interface {
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Collection
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
const PubSubCollectionEventName = "Collection"
|
||||||
|
|
||||||
|
type CollectionEvent struct {
|
||||||
|
AssocDisassoc AssocDisassoc
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CollectionEvent) Name() string {
|
||||||
|
return PubSubCollectionEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Associate/Disassociate
|
||||||
|
// *********************
|
||||||
|
type AssocDisassoc interface {
|
||||||
|
GetAssocDisassoc() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Associate
|
||||||
|
// *********************
|
||||||
|
const Assoc = "Associate"
|
||||||
|
|
||||||
|
type AssociateEvent struct {
|
||||||
|
XMLName xml.Name `xml:"associate"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssociateEvent) GetAssocDisassoc() string {
|
||||||
|
return Assoc
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Disassociate
|
||||||
|
// *********************
|
||||||
|
const Disassoc = "Disassociate"
|
||||||
|
|
||||||
|
type DisassociateEvent struct {
|
||||||
|
XMLName xml.Name `xml:"disassociate"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DisassociateEvent) GetAssocDisassoc() string {
|
||||||
|
return Disassoc
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Configuration
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
const PubSubConfigEventName = "Configuration"
|
||||||
|
|
||||||
|
type ConfigurationEvent struct {
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Form *Form
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConfigurationEvent) Name() string {
|
||||||
|
return PubSubConfigEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Delete
|
||||||
|
// *********************
|
||||||
|
const PubSubDeleteEventName = "Delete"
|
||||||
|
|
||||||
|
type DeleteEvent struct {
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Redirect *RedirectEvent `xml:"redirect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c DeleteEvent) Name() string {
|
||||||
|
return PubSubConfigEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Redirect
|
||||||
|
// *********************
|
||||||
|
type RedirectEvent struct {
|
||||||
|
URI string `xml:"uri,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// List
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
const PubSubItemsEventName = "List"
|
||||||
|
|
||||||
|
type ItemsEvent struct {
|
||||||
|
XMLName xml.Name `xml:"items"`
|
||||||
|
Items []ItemEvent `xml:"item,omitempty"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Retract *RetractEvent `xml:"retract"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemEvent struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
Id string `xml:"id,attr,omitempty"`
|
||||||
|
Publisher string `xml:"publisher,attr,omitempty"`
|
||||||
|
Any *Node `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ItemsEvent) Name() string {
|
||||||
|
return PubSubItemsEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// List
|
||||||
|
// *********************
|
||||||
|
|
||||||
|
type RetractEvent struct {
|
||||||
|
XMLName xml.Name `xml:"retract"`
|
||||||
|
ID string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Purge
|
||||||
|
// *********************
|
||||||
|
const PubSubPurgeEventName = "Purge"
|
||||||
|
|
||||||
|
type PurgeEvent struct {
|
||||||
|
XMLName xml.Name `xml:"purge"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PurgeEvent) Name() string {
|
||||||
|
return PubSubPurgeEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************
|
||||||
|
// Subscription
|
||||||
|
// *********************
|
||||||
|
const PubSubSubscriptionEventName = "Subscription"
|
||||||
|
|
||||||
|
type SubscriptionEvent struct {
|
||||||
|
SubStatus string `xml:"subscription,attr,omitempty"`
|
||||||
|
Expiry string `xml:"expiry,attr,omitempty"`
|
||||||
|
SubInfo `xml:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SubscriptionEvent) Name() string {
|
||||||
|
return PubSubSubscriptionEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
pse.XMLName = start.Name
|
||||||
|
// decode inner elements
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var ee EventElement
|
||||||
|
switch tt := t.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
switch tt.Name.Local {
|
||||||
|
case "collection":
|
||||||
|
ee = &CollectionEvent{}
|
||||||
|
case "configuration":
|
||||||
|
ee = &ConfigurationEvent{}
|
||||||
|
case "delete":
|
||||||
|
ee = &DeleteEvent{}
|
||||||
|
case "items":
|
||||||
|
ee = &ItemsEvent{}
|
||||||
|
case "purge":
|
||||||
|
ee = &PurgeEvent{}
|
||||||
|
case "subscription":
|
||||||
|
ee = &SubscriptionEvent{}
|
||||||
|
default:
|
||||||
|
ee = nil
|
||||||
|
}
|
||||||
|
// known child element found, decode it
|
||||||
|
if ee != nil {
|
||||||
|
err = d.DecodeElement(ee, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pse.EventElement = ee
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
162
stanza/msg_pubsub_event_test.go
Normal file
162
stanza/msg_pubsub_event_test.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeMsgEvent(t *testing.T) {
|
||||||
|
str := `<message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo'>
|
||||||
|
<event xmlns='http://jabber.org/protocol/pubsub#event'>
|
||||||
|
<items node='princely_musings'>
|
||||||
|
<item id='ae890ac52d0df67ed7cfdf51b644e901'>
|
||||||
|
<entry xmlns='http://www.w3.org/2005/Atom'>
|
||||||
|
<title>Soliloquy</title>
|
||||||
|
<summary>
|
||||||
|
To be, or not to be: that is the question:
|
||||||
|
Whether 'tis nobler in the mind to suffer
|
||||||
|
The slings and arrows of outrageous fortune,
|
||||||
|
Or to take arms against a sea of troubles,
|
||||||
|
And by opposing end them?
|
||||||
|
</summary>
|
||||||
|
<link rel='alternate' type='text/html'
|
||||||
|
href='http://denmark.lit/2003/12/13/atom03'/>
|
||||||
|
<id>tag:denmark.lit,2003:entry-32397</id>
|
||||||
|
<published>2003-12-13T18:30:02Z</published>
|
||||||
|
<updated>2003-12-13T18:30:02Z</updated>
|
||||||
|
</entry>
|
||||||
|
</item>
|
||||||
|
</items>
|
||||||
|
</event>
|
||||||
|
</message>
|
||||||
|
`
|
||||||
|
parsedMessage := stanza.Message{}
|
||||||
|
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||||
|
t.Errorf("message receipt unmarshall error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedMessage.Body != "" {
|
||||||
|
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsedMessage.Extensions) < 1 {
|
||||||
|
t.Errorf("no extension found on parsed message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ext := parsedMessage.Extensions[0].(type) {
|
||||||
|
case *stanza.PubSubEvent:
|
||||||
|
if ext.XMLName.Local != "event" {
|
||||||
|
t.Fatalf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||||
|
}
|
||||||
|
tmp, ok := parsedMessage.Extensions[0].(*stanza.PubSubEvent)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||||
|
}
|
||||||
|
ie, ok := tmp.EventElement.(*stanza.ItemsEvent)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||||
|
}
|
||||||
|
if ie.Items[0].Any.Nodes[0].Content != "Soliloquy" {
|
||||||
|
t.Fatalf("could not read title ! Read this : %s", ie.Items[0].Any.Nodes[0].Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ie.Items[0].Any.Nodes) != 6 {
|
||||||
|
t.Fatalf("some nodes were not correctly parsed")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("could not find pubsub event extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeEvent(t *testing.T) {
|
||||||
|
expected := "<message><event xmlns=\"http://jabber.org/protocol/pubsub#event\">" +
|
||||||
|
"<items node=\"princely_musings\"><item id=\"ae890ac52d0df67ed7cfdf51b644e901\">" +
|
||||||
|
"<entry xmlns=\"http://www.w3.org/2005/Atom\"><title>My pub item title</title>" +
|
||||||
|
"<summary>My pub item content summary</summary><link rel=\"alternate\" " +
|
||||||
|
"type=\"text/html\" href=\"http://denmark.lit/2003/12/13/atom03\">" +
|
||||||
|
"</link><id>My pub item content ID</id><published>2003-12-13T18:30:02Z</published>" +
|
||||||
|
"<updated>2003-12-13T18:30:02Z</updated></entry></item></items></event></message>"
|
||||||
|
message := stanza.Message{
|
||||||
|
Extensions: []stanza.MsgExtension{
|
||||||
|
stanza.PubSubEvent{
|
||||||
|
EventElement: stanza.ItemsEvent{
|
||||||
|
Items: []stanza.ItemEvent{
|
||||||
|
{
|
||||||
|
Id: "ae890ac52d0df67ed7cfdf51b644e901",
|
||||||
|
Any: &stanza.Node{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://www.w3.org/2005/Atom",
|
||||||
|
Local: "entry",
|
||||||
|
},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "",
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Node: "princely_musings",
|
||||||
|
Retract: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := xml.Marshal(message)
|
||||||
|
if strings.TrimSpace(string(data)) != strings.TrimSpace(expected) {
|
||||||
|
t.Errorf("event was not encoded properly : \nexpected:%s \ngot: %s", expected, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,6 +24,6 @@ type ReceiptReceived struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "request"}, ReceiptRequest{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "request"}, ReceiptRequest{})
|
||||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "received"}, ReceiptReceived{})
|
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "received"}, ReceiptReceived{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,9 +46,18 @@ func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||||
start.Name = n.XMLName
|
start.Name = n.XMLName
|
||||||
|
|
||||||
err = e.EncodeToken(start)
|
err = e.EncodeToken(start)
|
||||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if n.Content != "" {
|
if n.Content != "" {
|
||||||
e.EncodeToken(xml.CharData(n.Content))
|
err = e.EncodeToken(xml.CharData(n.Content))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,10 @@ import (
|
||||||
func TestNode_Marshal(t *testing.T) {
|
func TestNode_Marshal(t *testing.T) {
|
||||||
jsonData := []byte("{\"key\":\"value\"}")
|
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{
|
iqResp.Any = &Node{
|
||||||
XMLName: xml.Name{Space: "myNS", Local: "space"},
|
XMLName: xml.Name{Space: "myNS", Local: "space"},
|
||||||
Content: string(jsonData),
|
Content: string(jsonData),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package stanza
|
package stanza
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
type StanzaType string
|
type StanzaType string
|
||||||
|
|
||||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
||||||
|
@ -23,3 +25,7 @@ const (
|
||||||
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
|
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
|
||||||
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
|
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s StanzaType) IsEmpty() bool {
|
||||||
|
return len(strings.TrimSpace(string(s))) == 0
|
||||||
|
}
|
||||||
|
|
|
@ -50,11 +50,20 @@ func InitStream(p *xml.Decoder) (sessionID string, err error) {
|
||||||
// TODO make auth and bind use NextPacket instead of directly NextStart
|
// TODO make auth and bind use NextPacket instead of directly NextStart
|
||||||
func NextPacket(p *xml.Decoder) (Packet, error) {
|
func NextPacket(p *xml.Decoder) (Packet, error) {
|
||||||
// Read start element to find out how we want to parse the XMPP packet
|
// Read start element to find out how we want to parse the XMPP packet
|
||||||
se, err := NextStart(p)
|
t, err := NextXmppToken(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ee, ok := t.(xml.EndElement); ok {
|
||||||
|
return decodeStream(p, ee)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not an end element, then must be a start
|
||||||
|
se, ok := t.(xml.StartElement)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("unknown token ")
|
||||||
|
}
|
||||||
// Decode one of the top level XMPP namespace
|
// Decode one of the top level XMPP namespace
|
||||||
switch se.Name.Space {
|
switch se.Name.Space {
|
||||||
case NSStream:
|
case NSStream:
|
||||||
|
@ -73,7 +82,29 @@ func NextPacket(p *xml.Decoder) (Packet, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan XML token stream to find next StartElement.
|
// NextXmppToken scans XML token stream to find next StartElement or stream EndElement.
|
||||||
|
// We need the EndElement scan, because we must register stream close tags
|
||||||
|
func NextXmppToken(p *xml.Decoder) (xml.Token, error) {
|
||||||
|
for {
|
||||||
|
t, err := p.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
return xml.StartElement{}, errors.New("connection closed")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
return t, nil
|
||||||
|
case xml.EndElement:
|
||||||
|
if t.Name.Space == NSStream && t.Name.Local == "stream" {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextStart scans XML token stream to find next StartElement.
|
||||||
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
|
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||||
for {
|
for {
|
||||||
t, err := p.Token()
|
t, err := p.Token()
|
||||||
|
@ -97,16 +128,29 @@ TODO: From all the decoder, we can return a pointer to the actual concrete type,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// decodeStream will fully decode a stream packet
|
// decodeStream will fully decode a stream packet
|
||||||
func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
func decodeStream(p *xml.Decoder, t xml.Token) (Packet, error) {
|
||||||
switch se.Name.Local {
|
if se, ok := t.(xml.StartElement); ok {
|
||||||
case "error":
|
switch se.Name.Local {
|
||||||
return streamError.decode(p, se)
|
case "error":
|
||||||
case "features":
|
return streamError.decode(p, se)
|
||||||
return streamFeatures.decode(p, se)
|
case "features":
|
||||||
default:
|
return streamFeatures.decode(p, se)
|
||||||
return nil, errors.New("unexpected XMPP packet " +
|
default:
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
return nil, errors.New("unexpected XMPP packet " +
|
||||||
|
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ee, ok := t.(xml.EndElement); ok {
|
||||||
|
if ee.Name.Local == "stream" {
|
||||||
|
return streamClose.decode(ee), nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("unexpected XMPP packet " +
|
||||||
|
ee.Name.Space + " <" + ee.Name.Local + "/>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not happen
|
||||||
|
return nil, errors.New("unexpected XML token ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeSASL decodes a packet related to SASL authentication.
|
// decodeSASL decodes a packet related to SASL authentication.
|
||||||
|
|
|
@ -15,7 +15,7 @@ type Tune struct {
|
||||||
Uri string `xml:"uri,omitempty"`
|
Uri string `xml:"uri,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mood defines deta model for XEP-0107 - User Mood
|
// Mood defines data model for XEP-0107 - User Mood
|
||||||
// See: https://xmpp.org/extensions/xep-0107.html
|
// See: https://xmpp.org/extensions/xep-0107.html
|
||||||
type Mood struct {
|
type Mood struct {
|
||||||
MsgExtension // Mood can be added as a message extension
|
MsgExtension // Mood can be added as a message extension
|
||||||
|
|
|
@ -144,5 +144,5 @@ func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
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{})
|
||||||
}
|
}
|
||||||
|
|
419
stanza/pubsub.go
419
stanza/pubsub.go
|
@ -2,39 +2,432 @@ package stanza
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PubSub struct {
|
type PubSubGeneric struct {
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
|
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
|
||||||
Publish *Publish
|
|
||||||
Retract *Retract
|
Create *Create `xml:"create,omitempty"`
|
||||||
// TODO <configure/>
|
Configure *Configure `xml:"configure,omitempty"`
|
||||||
|
|
||||||
|
Subscribe *SubInfo `xml:"subscribe,omitempty"`
|
||||||
|
SubOptions *SubOptions `xml:"options,omitempty"`
|
||||||
|
|
||||||
|
Publish *Publish `xml:"publish,omitempty"`
|
||||||
|
PublishOptions *PublishOptions `xml:"publish-options"`
|
||||||
|
|
||||||
|
Affiliations *Affiliations `xml:"affiliations,omitempty"`
|
||||||
|
Default *Default `xml:"default,omitempty"`
|
||||||
|
|
||||||
|
Items *Items `xml:"items,omitempty"`
|
||||||
|
Retract *Retract `xml:"retract,omitempty"`
|
||||||
|
Subscription *Subscription `xml:"subscription,omitempty"`
|
||||||
|
|
||||||
|
Subscriptions *Subscriptions `xml:"subscriptions,omitempty"`
|
||||||
|
// 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 *PubSub) Namespace() string {
|
func (p *PubSubGeneric) Namespace() string {
|
||||||
return p.XMLName.Space
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Affiliation struct {
|
||||||
|
AffiliationStatus string `xml:"affiliation"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubOptions struct {
|
||||||
|
SubInfo
|
||||||
|
Form *Form `xml:"x"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Configure struct {
|
||||||
|
Form *Form `xml:"x"`
|
||||||
|
}
|
||||||
|
type Default struct {
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
Form *Form `xml:"x"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscribe struct {
|
||||||
|
XMLName xml.Name `xml:"subscribe"`
|
||||||
|
SubInfo
|
||||||
|
}
|
||||||
|
type Unsubscribe struct {
|
||||||
|
XMLName xml.Name `xml:"unsubscribe"`
|
||||||
|
SubInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubInfo represents information about a subscription
|
||||||
|
// Node is the node related to the subscription
|
||||||
|
// Jid is the subscription JID of the subscribed entity
|
||||||
|
// SubID is the subscription ID
|
||||||
|
type SubInfo struct {
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Jid string `xml:"jid,attr,omitempty"`
|
||||||
|
// Sub ID is optional
|
||||||
|
SubId *string `xml:"subid,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate checks if a node and a jid are present in the sub info, and if this jid is valid.
|
||||||
|
func (si *SubInfo) validate() error {
|
||||||
|
// Requests MUST contain a valid JID
|
||||||
|
if _, err := NewJid(si.Jid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// SubInfo must contain both a valid JID and a node. See XEP-0060
|
||||||
|
if strings.TrimSpace(si.Node) == "" {
|
||||||
|
return errors.New("SubInfo must contain the node AND the subscriber JID in subscription config options requests")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the "5.6 Retrieve Subscriptions" of XEP-0060
|
||||||
|
type Subscriptions struct {
|
||||||
|
XMLName xml.Name `xml:"subscriptions"`
|
||||||
|
List []Subscription `xml:"subscription,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the "5.6 Retrieve Subscriptions" and the 6.1 Subscribe to a Node and so on of XEP-0060
|
||||||
|
type Subscription struct {
|
||||||
|
SubStatus string `xml:"subscription,attr,omitempty"`
|
||||||
|
SubInfo `xml:",omitempty"`
|
||||||
|
// Seems like we can't marshal a self-closing tag for now : https://github.com/golang/go/issues/21399
|
||||||
|
// subscribe-options should be like this as per XEP-0060:
|
||||||
|
// <subscribe-options>
|
||||||
|
// <required/>
|
||||||
|
// </subscribe-options>
|
||||||
|
// Used to indicate if configuration options is required.
|
||||||
|
Required *struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishOptions struct {
|
||||||
|
XMLName xml.Name `xml:"publish-options"`
|
||||||
|
Form *Form
|
||||||
|
}
|
||||||
|
|
||||||
type Publish struct {
|
type Publish struct {
|
||||||
XMLName xml.Name `xml:"publish"`
|
XMLName xml.Name `xml:"publish"`
|
||||||
Node string `xml:"node,attr"`
|
Node string `xml:"node,attr"`
|
||||||
Item Item
|
Items []Item `xml:"item,omitempty"` // xsd says there can be many. See also 12.10 Batch Processing of XEP-0060
|
||||||
|
}
|
||||||
|
|
||||||
|
type Items struct {
|
||||||
|
List []Item `xml:"item,omitempty"`
|
||||||
|
MaxItems int `xml:"max_items,attr,omitempty"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
SubId string `xml:"subid,attr,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
XMLName xml.Name `xml:"item"`
|
XMLName xml.Name `xml:"item"`
|
||||||
Id string `xml:"id,attr,omitempty"`
|
Id string `xml:"id,attr,omitempty"`
|
||||||
Tune *Tune
|
Publisher string `xml:"publisher,attr,omitempty"`
|
||||||
Mood *Mood
|
Any *Node `xml:",any"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Retract struct {
|
type Retract struct {
|
||||||
XMLName xml.Name `xml:"retract"`
|
XMLName xml.Name `xml:"retract"`
|
||||||
Node string `xml:"node,attr"`
|
Node string `xml:"node,attr"`
|
||||||
Notify string `xml:"notify,attr"`
|
Notify *bool `xml:"notify,attr,omitempty"`
|
||||||
Item Item
|
Items []Item `xml:"item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PubSubOption struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:x:data options"`
|
||||||
|
Form `xml:"x"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubRq builds a subscription request to a node at the given service.
|
||||||
|
// 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) {
|
||||||
|
if e := subInfo.validate(); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
|
||||||
|
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Subscribe: &subInfo,
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnsubRq builds an unsub request to a node at the given service.
|
||||||
|
// 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) {
|
||||||
|
if e := subInfo.validate(); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
|
||||||
|
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Unsubscribe: &subInfo,
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubOptsRq builds a request for the subscription options.
|
||||||
|
// 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) {
|
||||||
|
if e := subInfo.validate(); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
|
||||||
|
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
SubOptions: &SubOptions{
|
||||||
|
SubInfo: subInfo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
if e := subInfo.validate(); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
if form.Type != FormTypeSubmit {
|
||||||
|
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.Payload = &PubSubGeneric{
|
||||||
|
SubOptions: &SubOptions{
|
||||||
|
SubInfo: subInfo,
|
||||||
|
Form: form,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubAndConfig builds a subscribe request that contains configuration options for the service
|
||||||
|
// From XEP-0060 : The <options/> element MUST follow the <subscribe/> element and
|
||||||
|
// MUST NOT possess a 'node' attribute or 'jid' attribute,
|
||||||
|
// 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) {
|
||||||
|
if e := subInfo.validate(); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
if form.Type != FormTypeSubmit {
|
||||||
|
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.Payload = &PubSubGeneric{
|
||||||
|
Subscribe: &subInfo,
|
||||||
|
SubOptions: &SubOptions{
|
||||||
|
SubInfo: SubInfo{SubId: subInfo.SubId},
|
||||||
|
Form: form,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Items: &Items{Node: node},
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxItems != 0 {
|
||||||
|
ps, _ := iq.Payload.(*PubSubGeneric)
|
||||||
|
ps.Items.MaxItems = maxItems
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Items: &Items{Node: node,
|
||||||
|
List: []Item{
|
||||||
|
{
|
||||||
|
Id: itemId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||||
|
if strings.TrimSpace(nodeID) == "" {
|
||||||
|
return nil, errors.New("cannot publish without a target node ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Publish: &Publish{Node: nodeID, Items: []Item{item}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// "The <item/> element provided by the publisher MAY possess an 'id' attribute,
|
||||||
|
// specifying a unique ItemID for the item.
|
||||||
|
// If an ItemID is not provided in the publish request,
|
||||||
|
// the pubsub service MUST generate one and MUST ensure that it is unique for that node."
|
||||||
|
if strings.TrimSpace(pubItemID) != "" {
|
||||||
|
ps, _ := iq.Payload.(*PubSubGeneric)
|
||||||
|
ps.Publish.Items[0].Id = pubItemID
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||||
|
if strings.TrimSpace(nodeID) == "" {
|
||||||
|
return nil, errors.New("cannot publish without a target node ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// "The <retract/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||||
|
if strings.TrimSpace(nodeID) == "" {
|
||||||
|
return nil, errors.New("cannot delete item without a target node ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Create: &Create{Node: nodeID},
|
||||||
|
Configure: &Configure{Form: confForm},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Create: &Create{Node: nodeName},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Subscriptions: &Subscriptions{},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubGeneric{
|
||||||
|
Affiliations: &Affiliations{},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub", Local: "pubsub"}, PubSubGeneric{})
|
||||||
}
|
}
|
||||||
|
|
451
stanza/pubsub_owner.go
Normal file
451
stanza/pubsub_owner.go
Normal file
|
@ -0,0 +1,451 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type AffiliationsOwner struct {
|
||||||
|
XMLName xml.Name `xml:"affiliations"`
|
||||||
|
Affiliations []AffiliationOwner `xml:"affiliation,omitempty"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AffiliationsOwner) UseCase() string {
|
||||||
|
return "affiliations"
|
||||||
|
}
|
||||||
|
|
||||||
|
type AffiliationOwner struct {
|
||||||
|
XMLName xml.Name `xml:"affiliation"`
|
||||||
|
AffiliationStatus string `xml:"affiliation,attr"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AffiliationStatusMember = "member"
|
||||||
|
AffiliationStatusNone = "none"
|
||||||
|
AffiliationStatusOutcast = "outcast"
|
||||||
|
AffiliationStatusOwner = "owner"
|
||||||
|
AffiliationStatusPublisher = "publisher"
|
||||||
|
AffiliationStatusPublishOnly = "publish-only"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigureOwner struct {
|
||||||
|
XMLName xml.Name `xml:"configure"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Form *Form `xml:"x,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ConfigureOwner) UseCase() string {
|
||||||
|
return "configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultOwner struct {
|
||||||
|
XMLName xml.Name `xml:"default"`
|
||||||
|
Form *Form `xml:"x,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DefaultOwner) UseCase() string {
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteOwner struct {
|
||||||
|
XMLName xml.Name `xml:"delete"`
|
||||||
|
RedirectOwner *RedirectOwner `xml:"redirect,omitempty"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DeleteOwner) UseCase() string {
|
||||||
|
return "delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedirectOwner struct {
|
||||||
|
XMLName xml.Name `xml:"redirect"`
|
||||||
|
URI string `xml:"uri,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurgeOwner struct {
|
||||||
|
XMLName xml.Name `xml:"purge"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*PurgeOwner) UseCase() string {
|
||||||
|
return "purge"
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionsOwner struct {
|
||||||
|
XMLName xml.Name `xml:"subscriptions"`
|
||||||
|
Subscriptions []SubscriptionOwner `xml:"subscription"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SubscriptionsOwner) UseCase() string {
|
||||||
|
return "subscriptions"
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionOwner struct {
|
||||||
|
SubscriptionStatus string `xml:"subscription"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SubscriptionStatusNone = "none"
|
||||||
|
SubscriptionStatusPending = "pending"
|
||||||
|
SubscriptionStatusSubscribed = "subscribed"
|
||||||
|
SubscriptionStatusUnconfigured = "unconfigured"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubOwner{
|
||||||
|
OwnerUseCase: &ConfigureOwner{Node: nodeName},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
if strings.TrimSpace(nodeID) == "" {
|
||||||
|
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.Payload = &PubSubOwner{
|
||||||
|
OwnerUseCase: &DeleteOwner{Node: nodeID},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubOwner{
|
||||||
|
OwnerUseCase: &PurgeOwner{Node: nodeId},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubOwner{
|
||||||
|
OwnerUseCase: &DefaultOwner{},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApproveSubRequest creates a new sub approval response to a request from the service to the owner of the node
|
||||||
|
// In order to approve the request, the owner shall submit the form and set the "pubsub#allow" field to a value of "1" or "true"
|
||||||
|
// For tracking purposes the message MUST reflect the 'id' attribute originally provided in the request.
|
||||||
|
// See 8.6 Manage Subscription Requests
|
||||||
|
func NewApproveSubRequest(serviceId, reqID string, apprForm *Form) (Message, error) {
|
||||||
|
if serviceId == "" {
|
||||||
|
return Message{}, errors.New("need a target service serviceId send approval serviceId")
|
||||||
|
}
|
||||||
|
if reqID == "" {
|
||||||
|
return Message{}, errors.New("the request ID is empty but must be used for the approval")
|
||||||
|
}
|
||||||
|
if apprForm == nil {
|
||||||
|
return Message{}, errors.New("approval form is nil")
|
||||||
|
}
|
||||||
|
apprMess := NewMessage(Attrs{To: serviceId})
|
||||||
|
apprMess.Extensions = []MsgExtension{apprForm}
|
||||||
|
apprMess.Id = reqID
|
||||||
|
|
||||||
|
return apprMess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, 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",
|
||||||
|
Action: CommandActionExecute,
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGetPendingSubRequests creates a new request for all pending subscriptions to be approved on a given node
|
||||||
|
// 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) {
|
||||||
|
if sessionId == "" {
|
||||||
|
return nil, errors.New("the sessionId must be maintained for the command")
|
||||||
|
}
|
||||||
|
|
||||||
|
form := &Form{
|
||||||
|
Type: FormTypeSubmit,
|
||||||
|
Fields: []*Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}},
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var n Node
|
||||||
|
err = xml.Unmarshal(data, &n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
Action: CommandActionExecute,
|
||||||
|
SessionId: sessionId,
|
||||||
|
CommandElement: &n,
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, 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, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubOwner{
|
||||||
|
OwnerUseCase: &SubscriptionsOwner{Node: nodeID, Subscriptions: subs},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iq.Payload = &PubSubOwner{
|
||||||
|
OwnerUseCase: &AffiliationsOwner{
|
||||||
|
Node: nodeID,
|
||||||
|
Affiliations: newAffils,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return iq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAffiliationListRequest creates a request to list all affiliated entities
|
||||||
|
// See 8.9.1 Retrieve List List
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
for _, elt := range payload.Configure.Form.Fields {
|
||||||
|
fieldMap[elt.Var] = elt
|
||||||
|
}
|
||||||
|
return fieldMap, nil
|
||||||
|
case *PubSubOwner:
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
for _, elt := range co.Form.Fields {
|
||||||
|
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)
|
||||||
|
if iq.Any.XMLName.Local != "command" {
|
||||||
|
return nil, errors.New("this IQ does not contain a form")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nde := range iq.Any.Nodes {
|
||||||
|
if nde.XMLName.Local == "x" {
|
||||||
|
for _, n := range nde.Nodes {
|
||||||
|
if n.XMLName.Local == "field" {
|
||||||
|
f := Field{}
|
||||||
|
data, err := xml.Marshal(n)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = xml.Unmarshal(data, &f)
|
||||||
|
if err == nil {
|
||||||
|
fieldMap[f.Var] = &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fieldMap, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("this IQ does not contain a form")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pso *PubSubOwner) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
pso.XMLName = start.Name
|
||||||
|
// 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 "affiliations":
|
||||||
|
aff := AffiliationsOwner{}
|
||||||
|
err = d.DecodeElement(&aff, &tt)
|
||||||
|
pso.OwnerUseCase = &aff
|
||||||
|
case "configure":
|
||||||
|
co := ConfigureOwner{}
|
||||||
|
err = d.DecodeElement(&co, &tt)
|
||||||
|
pso.OwnerUseCase = &co
|
||||||
|
case "default":
|
||||||
|
def := DefaultOwner{}
|
||||||
|
err = d.DecodeElement(&def, &tt)
|
||||||
|
pso.OwnerUseCase = &def
|
||||||
|
case "delete":
|
||||||
|
del := DeleteOwner{}
|
||||||
|
err = d.DecodeElement(&del, &tt)
|
||||||
|
pso.OwnerUseCase = &del
|
||||||
|
case "purge":
|
||||||
|
pu := PurgeOwner{}
|
||||||
|
err = d.DecodeElement(&pu, &tt)
|
||||||
|
pso.OwnerUseCase = &pu
|
||||||
|
case "subscriptions":
|
||||||
|
subs := SubscriptionsOwner{}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub#owner", Local: "pubsub"}, PubSubOwner{})
|
||||||
|
}
|
885
stanza/pubsub_owner_test.go
Normal file
885
stanza/pubsub_owner_test.go
Normal file
|
@ -0,0 +1,885 @@
|
||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ******************************
|
||||||
|
// * 8.2 Configure a Node
|
||||||
|
// ******************************
|
||||||
|
func TestNewConfigureNode(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"get\" id=\"config1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <configure node=\"princely_musings\"></configure> " +
|
||||||
|
"</pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.OwnerUseCase == nil {
|
||||||
|
t.Fatalf("owner use case is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("owner use case is not a configure tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownrUsecase.Node == "" {
|
||||||
|
t.Fatalf("could not parse node from config tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConfigureNodeResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||||
|
<configure node="princely_musings">
|
||||||
|
<x type="form" xmlns="jabber:x:data">
|
||||||
|
<field type="hidden" var="FORM_TYPE">
|
||||||
|
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||||
|
</field>
|
||||||
|
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||||
|
<value>1028</value>
|
||||||
|
</field>
|
||||||
|
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||||
|
<option label="Never">
|
||||||
|
<value>never</value>
|
||||||
|
</option>
|
||||||
|
<option label="When a new subscription is processed">
|
||||||
|
<value>on_sub</value>
|
||||||
|
</option>
|
||||||
|
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||||
|
<value>on_sub_and_presence</value>
|
||||||
|
</option>
|
||||||
|
<value>never</value>
|
||||||
|
</field>
|
||||||
|
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||||
|
<option>
|
||||||
|
<value>normal</value>
|
||||||
|
</option>
|
||||||
|
<option>
|
||||||
|
<value>headline</value>
|
||||||
|
</option>
|
||||||
|
<value>headline</value>
|
||||||
|
</field>
|
||||||
|
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||||
|
<value>http://www.w3.org/2005/Atom</value>
|
||||||
|
</field>
|
||||||
|
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||||
|
</x>
|
||||||
|
</configure>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubOwnerPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
if pubsub.OwnerUseCase == nil {
|
||||||
|
t.Fatalf("owner use case is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("owner use case is not a configure tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownrUsecase.Form == nil {
|
||||||
|
t.Fatalf("form is nil in the parsed config tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ownrUsecase.Form.Fields) != 8 {
|
||||||
|
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************
|
||||||
|
// * 8.3 Request Default Node Configuration Options
|
||||||
|
// *************************************************
|
||||||
|
|
||||||
|
func TestNewRequestDefaultConfig(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"get\" id=\"def1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <default></default> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.OwnerUseCase == nil {
|
||||||
|
t.Fatalf("owner use case is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = pubsub.OwnerUseCase.(*stanza.DefaultOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("owner use case is not a default tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRequestDefaultConfigResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||||
|
<configure node="princely_musings">
|
||||||
|
<x type="form" xmlns="jabber:x:data">
|
||||||
|
<field type="hidden" var="FORM_TYPE">
|
||||||
|
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||||
|
</field>
|
||||||
|
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||||
|
<value>1028</value>
|
||||||
|
</field>
|
||||||
|
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||||
|
<option label="Never">
|
||||||
|
<value>never</value>
|
||||||
|
</option>
|
||||||
|
<option label="When a new subscription is processed">
|
||||||
|
<value>on_sub</value>
|
||||||
|
</option>
|
||||||
|
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||||
|
<value>on_sub_and_presence</value>
|
||||||
|
</option>
|
||||||
|
<value>never</value>
|
||||||
|
</field>
|
||||||
|
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||||
|
<option>
|
||||||
|
<value>normal</value>
|
||||||
|
</option>
|
||||||
|
<option>
|
||||||
|
<value>headline</value>
|
||||||
|
</option>
|
||||||
|
<value>headline</value>
|
||||||
|
</field>
|
||||||
|
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||||
|
<value>http://www.w3.org/2005/Atom</value>
|
||||||
|
</field>
|
||||||
|
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||||
|
</x>
|
||||||
|
</configure>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubOwnerPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
if pubsub.OwnerUseCase == nil {
|
||||||
|
t.Fatalf("owner use case is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("owner use case is not a configure tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownrUsecase.Form == nil {
|
||||||
|
t.Fatalf("form is nil in the parsed config tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ownrUsecase.Form.Fields) != 8 {
|
||||||
|
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***********************
|
||||||
|
// * 8.4 Delete a Node
|
||||||
|
// ***********************
|
||||||
|
|
||||||
|
func TestNewDelNode(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\" id=\"delete1\" to=\"pubsub.shakespeare.lit\" >" +
|
||||||
|
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||||
|
"<delete node=\"princely_musings\"></delete> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.OwnerUseCase == nil {
|
||||||
|
t.Fatalf("owner use case is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = pubsub.OwnerUseCase.(*stanza.DeleteOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("owner use case is not a delete tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDelNodeResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq id="delete1" to="pubsub.shakespeare.lit" type="set">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||||
|
<delete node="princely_musings">
|
||||||
|
<redirect uri="xmpp:hamlet@denmark.lit"/>
|
||||||
|
</delete>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubOwnerPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
if pubsub.OwnerUseCase == nil {
|
||||||
|
t.Fatalf("owner use case is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.DeleteOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("owner use case is not a configure tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownrUsecase.RedirectOwner == nil {
|
||||||
|
t.Fatalf("redirect is nil in the delete tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownrUsecase.RedirectOwner.URI == "" {
|
||||||
|
t.Fatalf("could not parse redirect uri")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ****************************
|
||||||
|
// * 8.5 Purge All Node Items
|
||||||
|
// ****************************
|
||||||
|
|
||||||
|
func TestNewPurgeAllItems(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\" id=\"purge1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||||
|
"<purge node=\"princely_musings\"></purge> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.OwnerUseCase == nil {
|
||||||
|
t.Fatalf("owner use case is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
purge, ok := pubsub.OwnerUseCase.(*stanza.PurgeOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("owner use case is not a delete tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if purge.Node == "" {
|
||||||
|
t.Fatalf("could not parse purge targer node")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ************************************
|
||||||
|
// * 8.6 Manage Subscription Requests
|
||||||
|
// ************************************
|
||||||
|
func TestNewApproveSubRequest(t *testing.T) {
|
||||||
|
expectedReq := "<message id=\"approve1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||||
|
"<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value> </field> <field var=\"pubsub#subid\">" +
|
||||||
|
" <value>123-abc</value> </field> <field var=\"pubsub#node\"> <value>princely_musings</value> </field> " +
|
||||||
|
"<field var=\"pubsub#subscriber_jid\"> <value>horatio@denmark.lit</value> </field> <field var=\"pubsub#allow\"> " +
|
||||||
|
"<value>true</value> </field> </x> </message>"
|
||||||
|
|
||||||
|
apprForm := &stanza.Form{
|
||||||
|
Type: stanza.FormTypeSubmit,
|
||||||
|
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"}},
|
||||||
|
{Var: "pubsub#subscriber_jid", ValuesList: []string{"horatio@denmark.lit"}},
|
||||||
|
{Var: "pubsub#allow", ValuesList: []string{"true"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create a sub approval request: %v", err)
|
||||||
|
}
|
||||||
|
subR.Id = "approve1"
|
||||||
|
|
||||||
|
frm, ok := subR.Extensions[0].(*stanza.Form)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("extension is not a from !")
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowField *stanza.Field
|
||||||
|
|
||||||
|
for _, f := range frm.Fields {
|
||||||
|
if f.Var == "pubsub#allow" {
|
||||||
|
allowField = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allowField == nil || allowField.ValuesList[0] != "true" {
|
||||||
|
t.Fatalf("could not correctly parse the allow field in the response from")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************************************
|
||||||
|
// * 8.7 Process Pending Subscription Requests
|
||||||
|
// ********************************************
|
||||||
|
|
||||||
|
func TestNewGetPendingSubRequests(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\" id=\"pending1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||||
|
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\" node=\"http://jabber.org/protocol/pubsub#get-pending\" >" +
|
||||||
|
"</command> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
command, ok := subR.Payload.(*stanza.Command)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a command !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if command.Action != stanza.CommandActionExecute {
|
||||||
|
t.Fatalf("command should be execute !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if command.Node != "http://jabber.org/protocol/pubsub#get-pending" {
|
||||||
|
t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGetPendingSubRequestsResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||||
|
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
|
||||||
|
<x type="form" xmlns="jabber:x:data">
|
||||||
|
<field type="hidden" var="FORM_TYPE">
|
||||||
|
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
|
||||||
|
</field>
|
||||||
|
<field type="list-single" var="pubsub#node">
|
||||||
|
<option>
|
||||||
|
<value>princely_musings</value>
|
||||||
|
</option>
|
||||||
|
<option>
|
||||||
|
<value>news_from_elsinore</value>
|
||||||
|
</option>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
var respIQ stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse iq")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := respIQ.Payload.(*stanza.Command)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("this iq payload is not a command")
|
||||||
|
}
|
||||||
|
|
||||||
|
fMap, err := respIQ.GetFormFields()
|
||||||
|
if err != nil || len(fMap) != 2 {
|
||||||
|
t.Fatal("could not parse command form fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************************************
|
||||||
|
// * 8.7 Process Pending Subscription Requests
|
||||||
|
// ********************************************
|
||||||
|
|
||||||
|
func TestNewApprovePendingSubRequest(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\" id=\"pending2\" to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\"" +
|
||||||
|
"node=\"http://jabber.org/protocol/pubsub#get-pending\"sessionid=\"pubsub-get-pending:20031021T150901Z-600\"> " +
|
||||||
|
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field xmlns=\"jabber:x:data\" var=\"pubsub#node\"> " +
|
||||||
|
"<value xmlns=\"jabber:x:data\">princely_musings</value> </field> </x> </command> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit",
|
||||||
|
"pubsub-get-pending:20031021T150901Z-600",
|
||||||
|
"princely_musings")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
command, ok := subR.Payload.(*stanza.Command)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a command !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if command.Action != stanza.CommandActionExecute {
|
||||||
|
t.Fatalf("command should be execute !")
|
||||||
|
}
|
||||||
|
|
||||||
|
//if command.Node != "http://jabber.org/protocol/pubsub#get-pending"{
|
||||||
|
// t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !")
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************************************
|
||||||
|
// * 8.8.1 Retrieve Subscriptions List
|
||||||
|
// ********************************************
|
||||||
|
|
||||||
|
func TestNewSubListRqPl(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"get\" id=\"subman1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||||
|
"<subscriptions node=\"princely_musings\"></subscriptions> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||||
|
}
|
||||||
|
|
||||||
|
subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("pubsub doesn not contain a subscriptions node !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subs.Node != "princely_musings" {
|
||||||
|
t.Fatalf("subs node attribute should be princely_musings. Found %s", subs.Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSubListRqPlResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq from="pubsub.shakespeare.lit" id="subman1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||||
|
<subscriptions node="princely_musings">
|
||||||
|
<subscription jid="hamlet@denmark.lit" subscription="subscribed"></subscription>
|
||||||
|
<subscription jid="polonius@denmark.lit" subscription="unconfigured"></subscription>
|
||||||
|
<subscription jid="bernardo@denmark.lit" subid="123-abc" subscription="subscribed"></subscription>
|
||||||
|
<subscription jid="bernardo@denmark.lit" subid="004-yyy" subscription="subscribed"></subscription>
|
||||||
|
</subscriptions>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
var respIQ stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse iq")
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("this iq payload is not a command")
|
||||||
|
}
|
||||||
|
|
||||||
|
subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("pubsub doesn not contain a subscriptions node !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subs.Subscriptions) != 4 {
|
||||||
|
t.Fatalf("expected to find 4 subscriptions but got %d", len(subs.Subscriptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************************************
|
||||||
|
// * 8.9.1 Retrieve Affiliations List
|
||||||
|
// ********************************************
|
||||||
|
|
||||||
|
func TestNewAffiliationListRequest(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"get\" id=\"ent1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||||
|
"<affiliations node=\"princely_musings\"></affiliations> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||||
|
}
|
||||||
|
|
||||||
|
affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("pubsub doesn not contain an affiliations node !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if affils.Node != "princely_musings" {
|
||||||
|
t.Fatalf("affils node attribute should be princely_musings. Found %s", affils.Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAffiliationListRequestResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq from="pubsub.shakespeare.lit" id="ent1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||||
|
<affiliations node="princely_musings">
|
||||||
|
<affiliation affiliation="owner" jid="hamlet@denmark.lit"/>
|
||||||
|
<affiliation affiliation="outcast" jid="polonius@denmark.lit"/>
|
||||||
|
</affiliations>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
var respIQ stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse iq")
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("this iq payload is not a command")
|
||||||
|
}
|
||||||
|
|
||||||
|
affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("pubsub doesn not contain an affiliations node !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(affils.Affiliations) != 2 {
|
||||||
|
t.Fatalf("expected to find 2 subscriptions but got %d", len(affils.Affiliations))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************************************
|
||||||
|
// * 8.9.2 Modify Affiliation
|
||||||
|
// ********************************************
|
||||||
|
|
||||||
|
func TestNewModifAffiliationRequest(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\" id=\"ent3\" to=\"pubsub.shakespeare.lit\" > " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <affiliations node=\"princely_musings\"> " +
|
||||||
|
"<affiliation affiliation=\"none\" jid=\"hamlet@denmark.lit\"></affiliation> " +
|
||||||
|
"<affiliation affiliation=\"none\" jid=\"polonius@denmark.lit\"></affiliation> " +
|
||||||
|
"<affiliation affiliation=\"publisher\" jid=\"bard@shakespeare.lit\"></affiliation> </affiliations> </pubsub> " +
|
||||||
|
"</iq>"
|
||||||
|
|
||||||
|
affils := []stanza.AffiliationOwner{
|
||||||
|
{
|
||||||
|
AffiliationStatus: stanza.AffiliationStatusNone,
|
||||||
|
Jid: "hamlet@denmark.lit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AffiliationStatus: stanza.AffiliationStatusNone,
|
||||||
|
Jid: "polonius@denmark.lit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AffiliationStatus: stanza.AffiliationStatusPublisher,
|
||||||
|
Jid: "bard@shakespeare.lit",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||||
|
}
|
||||||
|
|
||||||
|
as, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("pubsub doesn not contain an affiliations node !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if as.Node != "princely_musings" {
|
||||||
|
t.Fatalf("affils node attribute should be princely_musings. Found %s", as.Node)
|
||||||
|
}
|
||||||
|
if len(as.Affiliations) != 3 {
|
||||||
|
t.Fatalf("expected 3 affiliations, found %d", len(as.Affiliations))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFormFields(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||||
|
<configure node="princely_musings">
|
||||||
|
<x type="form" xmlns="jabber:x:data">
|
||||||
|
<field type="hidden" var="FORM_TYPE">
|
||||||
|
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||||
|
</field>
|
||||||
|
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||||
|
<value>1028</value>
|
||||||
|
</field>
|
||||||
|
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||||
|
<option label="Never">
|
||||||
|
<value>never</value>
|
||||||
|
</option>
|
||||||
|
<option label="When a new subscription is processed">
|
||||||
|
<value>on_sub</value>
|
||||||
|
</option>
|
||||||
|
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||||
|
<value>on_sub_and_presence</value>
|
||||||
|
</option>
|
||||||
|
<value>never</value>
|
||||||
|
</field>
|
||||||
|
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||||
|
<option>
|
||||||
|
<value>normal</value>
|
||||||
|
</option>
|
||||||
|
<option>
|
||||||
|
<value>headline</value>
|
||||||
|
</option>
|
||||||
|
<value>headline</value>
|
||||||
|
</field>
|
||||||
|
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||||
|
<value>http://www.w3.org/2005/Atom</value>
|
||||||
|
</field>
|
||||||
|
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||||
|
</x>
|
||||||
|
</configure>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
var iq stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse IQ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fields, err := iq.GetFormFields()
|
||||||
|
if len(fields) != 8 {
|
||||||
|
t.Fatalf("could not correctly parse fields. Expected 8, found : %v", len(fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFormFieldsCmd(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||||
|
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
|
||||||
|
<x type="form" xmlns="jabber:x:data">
|
||||||
|
<field type="hidden" var="FORM_TYPE">
|
||||||
|
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
|
||||||
|
</field>
|
||||||
|
<field type="list-single" var="pubsub#node">
|
||||||
|
<option>
|
||||||
|
<value>princely_musings</value>
|
||||||
|
</option>
|
||||||
|
<option>
|
||||||
|
<value>news_from_elsinore</value>
|
||||||
|
</option>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
var iq stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse IQ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fields, err := iq.GetFormFields()
|
||||||
|
if len(fields) != 2 {
|
||||||
|
t.Fatalf("could not correctly parse fields. Expected 2, found : %v", len(fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &stanza.PubSubOwner{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("this iq payload is not a pubsub of the owner namespace")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubsub, nil
|
||||||
|
}
|
922
stanza/pubsub_test.go
Normal file
922
stanza/pubsub_test.go
Normal file
|
@ -0,0 +1,922 @@
|
||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"}},
|
||||||
|
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
|
||||||
|
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
|
||||||
|
{Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}},
|
||||||
|
{
|
||||||
|
Var: "pubsub#notification_type",
|
||||||
|
Type: "list-single",
|
||||||
|
Label: "Specify the delivery style for event notifications",
|
||||||
|
ValuesList: []string{"headline"},
|
||||||
|
Options: []stanza.Option{
|
||||||
|
{ValuesList: []string{"normal"}},
|
||||||
|
{ValuesList: []string{"headline"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, stanza.FormTypeSubmit)
|
||||||
|
|
||||||
|
// ***********************************
|
||||||
|
// * 6.1 Subscribe to a Node
|
||||||
|
// ***********************************
|
||||||
|
|
||||||
|
func TestNewSubRequest(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></subscribe>" +
|
||||||
|
" </pubsub> </iq>"
|
||||||
|
|
||||||
|
subInfo := stanza.SubInfo{
|
||||||
|
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||||
|
}
|
||||||
|
subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSubResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<subscription node="princely_musings" jid="francisco@denmark.lit"
|
||||||
|
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubGenericPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Subscription == nil {
|
||||||
|
t.Fatalf("subscription node is nil")
|
||||||
|
}
|
||||||
|
if pubsub.Subscription.Node == "" ||
|
||||||
|
pubsub.Subscription.Jid == "" ||
|
||||||
|
pubsub.Subscription.SubId == nil ||
|
||||||
|
pubsub.Subscription.SubStatus == "" {
|
||||||
|
t.Fatalf("one or more of the subscription attributes was not successfully decoded")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***********************************
|
||||||
|
// * 6.2 Unsubscribe from a Node
|
||||||
|
// ***********************************
|
||||||
|
|
||||||
|
func TestNewUnSubRequest(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\"id=\"unsub1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||||
|
"<unsubscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></unsubscribe> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subInfo := stanza.SubInfo{
|
||||||
|
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||||
|
}
|
||||||
|
subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.Unsubscribe == nil {
|
||||||
|
t.Fatalf("Unsubscribe tag should be present in sub config options request")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewUnsubResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="unsub1">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<subscription node="princely_musings" jid="francisco@denmark.lit" subscription="none"
|
||||||
|
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3"/>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubGenericPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Subscription == nil {
|
||||||
|
t.Fatalf("subscription node is nil")
|
||||||
|
}
|
||||||
|
if pubsub.Subscription.Node == "" ||
|
||||||
|
pubsub.Subscription.Jid == "" ||
|
||||||
|
pubsub.Subscription.SubId == nil ||
|
||||||
|
pubsub.Subscription.SubStatus == "" {
|
||||||
|
t.Fatalf("one or more of the subscription attributes was not successfully decoded")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 6.3 Configure Subscription Options
|
||||||
|
// ***************************************
|
||||||
|
func TestNewSubOptsRq(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"get\"id=\"options1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||||
|
"<options node=\"princely_musings\" jid=\"francisco@denmark.lit\"></options> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subInfo := stanza.SubInfo{
|
||||||
|
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||||
|
}
|
||||||
|
subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.SubOptions == nil {
|
||||||
|
t.Fatalf("Options tag should be present in sub config options request")
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNewConfOptsRsp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="options1">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<options node="princely_musings" jid="francisco@denmark.lit">
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="FORM_TYPE" type="hidden">
|
||||||
|
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#deliver" type="boolean" label="Enable delivery?">
|
||||||
|
<value>1</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#digest" type="boolean"
|
||||||
|
label="Receive digest notifications (approx. one per day)?">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#include_body" type="boolean"
|
||||||
|
label="Receive message body in addition to payload?">
|
||||||
|
<value>false</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#show-values" type="list-multi"
|
||||||
|
label="Select the presence types which are
|
||||||
|
allowed to receive event notifications">
|
||||||
|
<option label="Want to Chat">
|
||||||
|
<value>chat</value>
|
||||||
|
</option>
|
||||||
|
<option label="Available">
|
||||||
|
<value>online</value>
|
||||||
|
</option>
|
||||||
|
<option label="Away">
|
||||||
|
<value>away</value>
|
||||||
|
</option>
|
||||||
|
<option label="Extended Away">
|
||||||
|
<value>xa</value>
|
||||||
|
</option>
|
||||||
|
<option label="Do Not Disturb">
|
||||||
|
<value>dnd</value>
|
||||||
|
</option>
|
||||||
|
<value>chat</value>
|
||||||
|
<value>online</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</options>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubGenericPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.SubOptions == nil {
|
||||||
|
t.Fatalf("sub options node is nil")
|
||||||
|
}
|
||||||
|
if pubsub.SubOptions.Form == nil {
|
||||||
|
t.Fatalf("the response form is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pubsub.SubOptions.Form.Fields) != 5 {
|
||||||
|
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 6.3.5 Form Submission
|
||||||
|
// ***************************************
|
||||||
|
func TestNewFormSubmission(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\" id=\"options2\" to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <options node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
|
||||||
|
"<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#title\"> " +
|
||||||
|
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
|
||||||
|
"<value>1</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> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
|
||||||
|
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
|
||||||
|
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
|
||||||
|
"</field> </x> </options> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subInfo := stanza.SubInfo{
|
||||||
|
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||||
|
}
|
||||||
|
|
||||||
|
subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.SubOptions == nil {
|
||||||
|
t.Fatalf("Options tag should be present in sub config options request")
|
||||||
|
}
|
||||||
|
if pubsub.SubOptions.Form == nil {
|
||||||
|
t.Fatalf("No form in form submit request !")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 6.3.7 Subscribe and Configure
|
||||||
|
// ***************************************
|
||||||
|
|
||||||
|
func TestNewSubAndConfig(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\">" +
|
||||||
|
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
|
||||||
|
"</subscribe>" +
|
||||||
|
"<options> <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#title\"> " +
|
||||||
|
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
|
||||||
|
"<value>1</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> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
|
||||||
|
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
|
||||||
|
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
|
||||||
|
"</field> </x> </options> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subInfo := stanza.SubInfo{
|
||||||
|
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||||
|
}
|
||||||
|
|
||||||
|
subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.SubOptions == nil {
|
||||||
|
t.Fatalf("Options tag should be present in sub config options request")
|
||||||
|
}
|
||||||
|
if pubsub.SubOptions.Form == nil {
|
||||||
|
t.Fatalf("No form in form submit request !")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The <options/> element MUST NOT possess a 'node' attribute or 'jid' attribute
|
||||||
|
// See XEP-0060
|
||||||
|
if pubsub.SubOptions.SubInfo.Node != "" || pubsub.SubOptions.SubInfo.Jid != "" {
|
||||||
|
t.Fatalf("SubInfo node and jid should be empty for the options tag !")
|
||||||
|
}
|
||||||
|
if pubsub.Subscribe.Node == "" || pubsub.Subscribe.Jid == "" {
|
||||||
|
t.Fatalf("SubInfo node and jid should NOT be empty for the subscribe tag !")
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSubAndConfigResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<subscription node="princely_musings" jid="francisco@denmark.lit"
|
||||||
|
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
|
||||||
|
<options>
|
||||||
|
<x xmlns="jabber:x:data" type="result">
|
||||||
|
<field var="FORM_TYPE" type="hidden">
|
||||||
|
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#deliver">
|
||||||
|
<value>1</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#digest">
|
||||||
|
<value>0</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#include_body">
|
||||||
|
<value>false</value>
|
||||||
|
</field>
|
||||||
|
<field var="pubsub#show-values">
|
||||||
|
<value>chat</value>
|
||||||
|
<value>online</value>
|
||||||
|
<value>away</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</options>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubGenericPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
if pubsub.Subscription == nil {
|
||||||
|
t.Fatalf("sub node is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.SubOptions == nil {
|
||||||
|
t.Fatalf("sub options node is nil")
|
||||||
|
}
|
||||||
|
if pubsub.SubOptions.Form == nil {
|
||||||
|
t.Fatalf("the response form is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pubsub.SubOptions.Form.Fields) != 5 {
|
||||||
|
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 6.5.2 Requesting All List
|
||||||
|
// ***************************************
|
||||||
|
func TestNewItemsRequest(t *testing.T) {
|
||||||
|
subR, err := stanza.NewItemsRequest("pubsub.shakespeare.lit", "princely_musings", 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create an items request : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, e := checkMarshalling(t, subR); e != nil {
|
||||||
|
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.Items == nil {
|
||||||
|
t.Fatalf("List tag should be present to request items from a service")
|
||||||
|
}
|
||||||
|
if len(pubsub.Items.List) != 0 {
|
||||||
|
t.Fatalf("There should be no items in the <items> tag to request all items from a service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestNewItemsResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="items2">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<items node="princely_musings">
|
||||||
|
<item id="4e30f35051b7b8b42abe083742187228">
|
||||||
|
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>Alone</title>
|
||||||
|
<summary> Now I am alone. O, what a rogue and peasant slave am I! </summary>
|
||||||
|
<link rel="alternate" type="text/html"
|
||||||
|
href="http://denmark.lit/2003/12/13/atom03"/>
|
||||||
|
<id>tag:denmark.lit,2003:entry-32396</id>
|
||||||
|
<published>2003-12-13T11:09:53Z</published>
|
||||||
|
<updated>2003-12-13T11:09:53Z</updated>
|
||||||
|
</entry>
|
||||||
|
</item>
|
||||||
|
<item id="ae890ac52d0df67ed7cfdf51b644e901">
|
||||||
|
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>Soliloquy</title>
|
||||||
|
<summary> To be, or not to be: that is the question: Whether 'tis nobler in the
|
||||||
|
mind to suffer The slings and arrows of outrageous fortune, Or to take arms
|
||||||
|
against a sea of troubles, And by opposing end them? </summary>
|
||||||
|
<link rel="alternate" type="text/html"
|
||||||
|
href="http://denmark.lit/2003/12/13/atom03"/>
|
||||||
|
<id>tag:denmark.lit,2003:entry-32397</id>
|
||||||
|
<published>2003-12-13T18:30:02Z</published>
|
||||||
|
<updated>2003-12-13T18:30:02Z</updated>
|
||||||
|
</entry>
|
||||||
|
</item>
|
||||||
|
</items>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
|
||||||
|
pubsub, err := getPubSubGenericPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
if pubsub.Items == nil {
|
||||||
|
t.Fatalf("sub options node is nil")
|
||||||
|
}
|
||||||
|
if pubsub.Items.List == nil {
|
||||||
|
t.Fatalf("the response form is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pubsub.Items.List) != 2 {
|
||||||
|
t.Fatalf("one or more items in the response could not be parsed correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 6.5.8 Requesting a Particular Item
|
||||||
|
// ***************************************
|
||||||
|
func TestNewSpecificItemRequest(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"get\" id=\"items3\"to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <items node=\"princely_musings\"> " +
|
||||||
|
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </items> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.Items == nil {
|
||||||
|
t.Fatalf("List tag should be present to request items from a service")
|
||||||
|
}
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 7.1 Publish an Item to a Node
|
||||||
|
// ***************************************
|
||||||
|
func TestNewPublishItemRq(t *testing.T) {
|
||||||
|
item := stanza.Item{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Id: "",
|
||||||
|
Publisher: "",
|
||||||
|
Any: &stanza.Node{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://www.w3.org/2005/Atom",
|
||||||
|
Local: "entry",
|
||||||
|
},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "",
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
subR, err := stanza.NewPublishItemRq("pubsub.shakespeare.lit", "princely_musings", "bnd81g37d61f49fgn581", item)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create an item pub request : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, e := checkMarshalling(t, subR); e != nil {
|
||||||
|
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(pubsub.Publish.Node) == "" {
|
||||||
|
t.Fatalf("the <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node.")
|
||||||
|
}
|
||||||
|
if pubsub.Publish.Items[0].Id == "" {
|
||||||
|
t.Fatalf("an id was provided for the item and it should be used")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 7.1.5 Publishing Options
|
||||||
|
// ***************************************
|
||||||
|
|
||||||
|
func TestNewPublishItemOptsRq(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\"id=\"pub1\"to=\"pubsub.shakespeare.lit\"> <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||||
|
"<publish node=\"princely_musings\"> <item id=\"ae890ac52d0df67ed7cfdf51b644e901\"> " +
|
||||||
|
"<entry xmlns=\"http://www.w3.org/2005/Atom\"> <title>Soliloquy</title> " +
|
||||||
|
"<summary> To be, or not to be: that is the question: Whether \"tis nobler in the mind to suffer The " +
|
||||||
|
"slings and arrows of outrageous fortune, Or to take arms against a sea of troubles, And by opposing end them? " +
|
||||||
|
"</summary> <link rel=\"alternate\" type=\"text/html\"href=\"http://denmark.lit/2003/12/13/atom03\"></link> " +
|
||||||
|
"<id>tag:denmark.lit,2003:entry-32397</id> <published>2003-12-13T18:30:02Z</published> " +
|
||||||
|
"<updated>2003-12-13T18:30:02Z</updated> </entry> </item> </publish> <publish-options> " +
|
||||||
|
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||||
|
"<value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var=\"pubsub#access_model\"> " +
|
||||||
|
"<value>presence</value> </field> </x> </publish-options> </pubsub> </iq>"
|
||||||
|
|
||||||
|
var iq stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(expectedReq), &iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not unmarshal example request : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := iq.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.Publish == nil {
|
||||||
|
t.Fatalf("Publish tag is empty")
|
||||||
|
}
|
||||||
|
if len(pubsub.Publish.Items) != 1 {
|
||||||
|
t.Fatalf("could not parse item properly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 7.2 Delete an Item from a Node
|
||||||
|
// ***************************************
|
||||||
|
|
||||||
|
func TestNewDelItemFromNode(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\"id=\"retract1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <retract node=\"princely_musings\"> " +
|
||||||
|
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </retract> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.Retract == nil {
|
||||||
|
t.Fatalf("Retract tag should be present to del an item from a service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(pubsub.Retract.Items[0].Id) == "" {
|
||||||
|
t.Fatalf("Item id, for the item to delete, should be non empty")
|
||||||
|
}
|
||||||
|
if pubsub.Retract.Items[0].Any != nil {
|
||||||
|
t.Fatalf("Item node must be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 8.1 Create a Node
|
||||||
|
// ***************************************
|
||||||
|
|
||||||
|
func TestNewCreateNode(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\"id=\"create1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.Create == nil {
|
||||||
|
t.Fatalf("Create tag should be present to create a node on a service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(pubsub.Create.Node) == "" {
|
||||||
|
t.Fatalf("Expected node name to be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCreateNodeResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="hamlet@denmark.lit/elsinore" id="create2">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<create node="25e3d37dabbab9541f7523321421edc5bfeb2dae"/>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
pubsub, err := getPubSubGenericPayload(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
if pubsub.Create == nil {
|
||||||
|
t.Fatalf("create segment is nil")
|
||||||
|
}
|
||||||
|
if pubsub.Create.Node == "" {
|
||||||
|
t.Fatalf("could not parse generated nodeId")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************
|
||||||
|
// * 8.1.3 Create and Configure a Node
|
||||||
|
// ***************************************
|
||||||
|
|
||||||
|
func TestNewCreateAndConfigNode(t *testing.T) {
|
||||||
|
expectedReq := "<iq type=\"set\" id=\"create1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> " +
|
||||||
|
"<configure> <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#notify_retract\"> " +
|
||||||
|
"<value>0</value> </field> <field var=\"pubsub#notify_sub\"> <value>0</value> </field> " +
|
||||||
|
"<field var=\"pubsub#max_payload_size\"> <value>1028</value> </field> </x> </configure> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewCreateAndConfigNode("pubsub.shakespeare.lit",
|
||||||
|
"princely_musings",
|
||||||
|
&stanza.Form{
|
||||||
|
Type: stanza.FormTypeSubmit,
|
||||||
|
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"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("payload is not a pubsub !")
|
||||||
|
}
|
||||||
|
if pubsub.Create == nil {
|
||||||
|
t.Fatalf("Create tag should be present to create a node on a service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(pubsub.Create.Node) == "" {
|
||||||
|
t.Fatalf("Expected node name to be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Configure == nil {
|
||||||
|
t.Fatalf("Configure tag should be present to configure a node during its creation on a service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Configure.Form == nil {
|
||||||
|
t.Fatalf("Expected a form to be present, to configure the node")
|
||||||
|
}
|
||||||
|
if len(pubsub.Configure.Form.Fields) != 4 {
|
||||||
|
t.Fatalf("Expected 4 elements to be present in the config form but got : %v", len(pubsub.Configure.Form.Fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************************
|
||||||
|
// * 5.7 Retrieve Subscriptions
|
||||||
|
// ********************************
|
||||||
|
|
||||||
|
func TestNewRetrieveAllSubsRequest(t *testing.T) {
|
||||||
|
expected := "<iq type=\"get\" id=\"subscriptions1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscriptions></subscriptions> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expected, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetrieveAllSubsResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="subscriptions1">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<subscriptions>
|
||||||
|
<subscription node="node1" jid="francisco@denmark.lit" subscription="subscribed"/>
|
||||||
|
<subscription node="node2" jid="francisco@denmark.lit" subscription="subscribed"/>
|
||||||
|
<subscription node="node5" jid="francisco@denmark.lit" subscription="unconfigured"/>
|
||||||
|
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
|
||||||
|
subid="123-abc"/>
|
||||||
|
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
|
||||||
|
subid="004-yyy"/>
|
||||||
|
</subscriptions>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
var respIQ stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not unmarshal response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("umarshalled payload is not a pubsub")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Subscriptions == nil {
|
||||||
|
t.Fatalf("subscriptions node is nil")
|
||||||
|
}
|
||||||
|
if len(pubsub.Subscriptions.List) != 5 {
|
||||||
|
t.Fatalf("incorrect number of decoded subscriptions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************************
|
||||||
|
// * 5.7 Retrieve Affiliations
|
||||||
|
// ********************************
|
||||||
|
|
||||||
|
func TestNewRetrieveAllAffilsRequest(t *testing.T) {
|
||||||
|
expected := "<iq type=\"get\"id=\"affil1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||||
|
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <affiliations></affiliations> </pubsub> </iq>"
|
||||||
|
|
||||||
|
subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(subR)
|
||||||
|
if err := compareMarshal(expected, string(data)); err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetrieveAllAffilsResp(t *testing.T) {
|
||||||
|
response := `
|
||||||
|
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="affil1">
|
||||||
|
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||||
|
<affiliations>
|
||||||
|
<affiliation node="node1" affiliation="owner"/>
|
||||||
|
<affiliation node="node2" affiliation="publisher"/>
|
||||||
|
<affiliation node="node5" affiliation="outcast"/>
|
||||||
|
<affiliation node="node6" affiliation="owner"/>
|
||||||
|
</affiliations>
|
||||||
|
</pubsub>
|
||||||
|
</iq>
|
||||||
|
`
|
||||||
|
var respIQ stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not unmarshal response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("umarshalled payload is not a pubsub")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Affiliations == nil {
|
||||||
|
t.Fatalf("subscriptions node is nil")
|
||||||
|
}
|
||||||
|
if len(pubsub.Affiliations.List) != 4 {
|
||||||
|
t.Fatalf("incorrect number of decoded subscriptions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) {
|
||||||
|
var respIQ stanza.IQ
|
||||||
|
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &stanza.PubSubGeneric{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("this iq payload is not a pubsub")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubsub, nil
|
||||||
|
}
|
29
stanza/results_sets.go
Normal file
29
stanza/results_sets.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Support for XEP-0059
|
||||||
|
// See https://xmpp.org/extensions/xep-0059
|
||||||
|
const (
|
||||||
|
// Common but not only possible namespace for query blocks in a result set context
|
||||||
|
NSQuerySet = "jabber:iq:search"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResultSet struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
|
||||||
|
After *string `xml:"after,omitempty"`
|
||||||
|
Before *string `xml:"before,omitempty"`
|
||||||
|
Count *int `xml:"count,omitempty"`
|
||||||
|
First *First `xml:"first,omitempty"`
|
||||||
|
Index *int `xml:"index,omitempty"`
|
||||||
|
Last *string `xml:"last,omitempty"`
|
||||||
|
Max *int `xml:"max,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type First struct {
|
||||||
|
XMLName xml.Name `xml:"first"`
|
||||||
|
Content string
|
||||||
|
Index *int `xml:"index,attr,omitempty"`
|
||||||
|
}
|
28
stanza/results_sets_test.go
Normal file
28
stanza/results_sets_test.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Limiting the number of items
|
||||||
|
func TestNewResultSetReq(t *testing.T) {
|
||||||
|
expectedRq := "<iq id=\"q29302\" type=\"set\"> <query xmlns=\"urn:xmpp:mam:2\"> " +
|
||||||
|
"<x type=\"submit\" xmlns=\"jabber:x:data\"> <field type=\"hidden\" var=\"FORM_TYPE\"> " +
|
||||||
|
"<value>urn:xmpp:mam:2</value> </field> <field var=\"start\"> <value>2010-08-07T00:00:00Z</value> </field> </x> " +
|
||||||
|
"<set xmlns=\"http://jabber.org/protocol/rsm\"> <max>10</max> </set> </query> </iq>"
|
||||||
|
|
||||||
|
maxVal := 10
|
||||||
|
rs := &stanza.ResultSet{
|
||||||
|
Max: &maxVal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO when Mam is implemented
|
||||||
|
_ = expectedRq
|
||||||
|
_ = rs
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalResultSeqReq(t *testing.T) {
|
||||||
|
// TODO when Mam is implemented
|
||||||
|
|
||||||
|
}
|
|
@ -69,12 +69,18 @@ type Bind struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
||||||
Resource string `xml:"resource,omitempty"`
|
Resource string `xml:"resource,omitempty"`
|
||||||
Jid string `xml:"jid,omitempty"`
|
Jid string `xml:"jid,omitempty"`
|
||||||
|
// Result sets
|
||||||
|
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bind) Namespace() string {
|
func (b *Bind) Namespace() string {
|
||||||
return b.XMLName.Space
|
return b.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bind) GetSet() *ResultSet {
|
||||||
|
return b.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Session (Obsolete)
|
// Session (Obsolete)
|
||||||
|
|
||||||
|
@ -87,17 +93,23 @@ func (b *Bind) Namespace() string {
|
||||||
// This is the draft defining how to handle the transition:
|
// This is the draft defining how to handle the transition:
|
||||||
// https://tools.ietf.org/html/draft-cridland-xmpp-session-01
|
// https://tools.ietf.org/html/draft-cridland-xmpp-session-01
|
||||||
type StreamSession struct {
|
type StreamSession struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
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
|
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 {
|
func (s *StreamSession) Namespace() string {
|
||||||
return s.XMLName.Space
|
return s.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StreamSession) GetSet() *ResultSet {
|
||||||
|
return s.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StreamSession) IsOptional() bool {
|
func (s *StreamSession) IsOptional() bool {
|
||||||
if s.XMLName.Local == "session" {
|
if s.XMLName.Local == "session" {
|
||||||
return s.Optional
|
return s.Optional != nil
|
||||||
}
|
}
|
||||||
// If session element is missing, then we should not use session
|
// If session element is missing, then we should not use session
|
||||||
return true
|
return true
|
||||||
|
@ -107,6 +119,6 @@ func (s *StreamSession) IsOptional() bool {
|
||||||
// Registry init
|
// Registry init
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, Bind{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-bind", Local: "bind"}, Bind{})
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-session", "session"}, StreamSession{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-session", Local: "session"}, StreamSession{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
// Check that we can detect optional session from advertised stream features
|
// Check that we can detect optional session from advertised stream features
|
||||||
func TestSessionFeatures(t *testing.T) {
|
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)
|
data, err := xml.Marshal(streamFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -28,8 +28,11 @@ func TestSessionFeatures(t *testing.T) {
|
||||||
|
|
||||||
// Check that the Session tag can be used in IQ decoding
|
// Check that the Session tag can be used in IQ decoding
|
||||||
func TestSessionIQ(t *testing.T) {
|
func TestSessionIQ(t *testing.T) {
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"})
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"})
|
||||||
iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: true}
|
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)
|
data, err := xml.Marshal(iq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
171
stanza/stanza_errors.go
Normal file
171
stanza/stanza_errors.go
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StanzaErrorGroup interface {
|
||||||
|
GroupErrorName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadFormat struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BadFormat) GroupErrorName() string { return "bad-format" }
|
||||||
|
|
||||||
|
type BadNamespacePrefix struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-namespace-prefix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BadNamespacePrefix) GroupErrorName() string { return "bad-namespace-prefix" }
|
||||||
|
|
||||||
|
type Conflict struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas conflict"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Conflict) GroupErrorName() string { return "conflict" }
|
||||||
|
|
||||||
|
type ConnectionTimeout struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas connection-timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnectionTimeout) GroupErrorName() string { return "connection-timeout" }
|
||||||
|
|
||||||
|
type HostGone struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-gone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HostGone) GroupErrorName() string { return "host-gone" }
|
||||||
|
|
||||||
|
type HostUnknown struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-unknown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HostUnknown) GroupErrorName() string { return "host-unknown" }
|
||||||
|
|
||||||
|
type ImproperAddressing struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas improper-addressing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ImproperAddressing) GroupErrorName() string { return "improper-addressing" }
|
||||||
|
|
||||||
|
type InternalServerError struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InternalServerError) GroupErrorName() string { return "internal-server-error" }
|
||||||
|
|
||||||
|
type InvalidForm struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-from"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidForm) GroupErrorName() string { return "invalid-from" }
|
||||||
|
|
||||||
|
type InvalidId struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidId) GroupErrorName() string { return "invalid-id" }
|
||||||
|
|
||||||
|
type InvalidNamespace struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-namespace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidNamespace) GroupErrorName() string { return "invalid-namespace" }
|
||||||
|
|
||||||
|
type InvalidXML struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-xml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidXML) GroupErrorName() string { return "invalid-xml" }
|
||||||
|
|
||||||
|
type NotAuthorized struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-authorized"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NotAuthorized) GroupErrorName() string { return "not-authorized" }
|
||||||
|
|
||||||
|
type NotWellFormed struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-well-formed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NotWellFormed) GroupErrorName() string { return "not-well-formed" }
|
||||||
|
|
||||||
|
type PolicyViolation struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas policy-violation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PolicyViolation) GroupErrorName() string { return "policy-violation" }
|
||||||
|
|
||||||
|
type RemoteConnectionFailed struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas remote-connection-failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RemoteConnectionFailed) GroupErrorName() string { return "remote-connection-failed" }
|
||||||
|
|
||||||
|
type Reset struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas reset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Reset) GroupErrorName() string { return "reset" }
|
||||||
|
|
||||||
|
type ResourceConstraint struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ResourceConstraint) GroupErrorName() string { return "resource-constraint" }
|
||||||
|
|
||||||
|
type RestrictedXML struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas restricted-xml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RestrictedXML) GroupErrorName() string { return "restricted-xml" }
|
||||||
|
|
||||||
|
type SeeOtherHost struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas see-other-host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SeeOtherHost) GroupErrorName() string { return "see-other-host" }
|
||||||
|
|
||||||
|
type SystemShutdown struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas system-shutdown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SystemShutdown) GroupErrorName() string { return "system-shutdown" }
|
||||||
|
|
||||||
|
type UndefinedCondition struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UndefinedCondition) GroupErrorName() string { return "undefined-condition" }
|
||||||
|
|
||||||
|
type UnsupportedEncoding struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-encoding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnexpectedRequest struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnexpectedRequest) GroupErrorName() string { return "unexpected-request" }
|
||||||
|
|
||||||
|
func (e *UnsupportedEncoding) GroupErrorName() string { return "unsupported-encoding" }
|
||||||
|
|
||||||
|
type UnsupportedStanzaType struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-stanza-type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnsupportedStanzaType) GroupErrorName() string { return "unsupported-stanza-type" }
|
||||||
|
|
||||||
|
type UnsupportedVersion struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnsupportedVersion) GroupErrorName() string { return "unsupported-version" }
|
||||||
|
|
||||||
|
type XMLNotWellFormed struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas xml-not-well-formed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *XMLNotWellFormed) GroupErrorName() string { return "xml-not-well-formed" }
|
|
@ -8,7 +8,9 @@ import "encoding/xml"
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"`
|
XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"`
|
||||||
From string `xml:"from,attr"`
|
From string `xml:"from,attr"`
|
||||||
To string `xml:"to,attr"`
|
To string `xml:"to,attr"`
|
||||||
Id string `xml:"id,attr"`
|
Id string `xml:"id,attr"`
|
||||||
Version string `xml:"version,attr"`
|
Version string `xml:"version,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StreamClose = "</stream:stream>"
|
||||||
|
|
|
@ -15,7 +15,7 @@ type StreamFeatures struct {
|
||||||
// Server capabilities hash
|
// Server capabilities hash
|
||||||
Caps Caps
|
Caps Caps
|
||||||
// Stream features
|
// Stream features
|
||||||
StartTLS tlsStartTLS
|
StartTLS TlsStartTLS
|
||||||
Mechanisms saslMechanisms
|
Mechanisms saslMechanisms
|
||||||
Bind Bind
|
Bind Bind
|
||||||
StreamManagement streamManagement
|
StreamManagement streamManagement
|
||||||
|
@ -60,13 +60,13 @@ type Caps struct {
|
||||||
|
|
||||||
// StartTLS feature
|
// StartTLS feature
|
||||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
|
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
|
||||||
type tlsStartTLS struct {
|
type TlsStartTLS struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
||||||
Required bool
|
Required bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing startTLS required flag
|
// UnmarshalXML implements custom parsing startTLS required flag
|
||||||
func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (stls *TlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
stls.XMLName = start.Name
|
stls.XMLName = start.Name
|
||||||
|
|
||||||
// Check subelements to extract required field as boolean
|
// Check subelements to extract required field as boolean
|
||||||
|
@ -98,7 +98,7 @@ func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStartTLS() (feature tlsStartTLS, isSupported bool) {
|
func (sf *StreamFeatures) DoesStartTLS() (feature TlsStartTLS, isSupported bool) {
|
||||||
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
||||||
return sf.StartTLS, true
|
return sf.StartTLS, true
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,10 @@ type streamManagement struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (streamManagement) Name() string {
|
||||||
|
return "streamManagement"
|
||||||
|
}
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
||||||
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
||||||
return true
|
return true
|
||||||
|
@ -165,3 +169,21 @@ func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamErr
|
||||||
err := p.DecodeElement(&packet, &se)
|
err := p.DecodeElement(&packet, &se)
|
||||||
return packet, err
|
return packet, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// StreamClose "Packet"
|
||||||
|
|
||||||
|
// This is just a closing tag and hold no information
|
||||||
|
type StreamClosePacket struct{}
|
||||||
|
|
||||||
|
func (StreamClosePacket) Name() string {
|
||||||
|
return "stream:stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamCloseDecoder struct{}
|
||||||
|
|
||||||
|
var streamClose streamCloseDecoder
|
||||||
|
|
||||||
|
func (streamCloseDecoder) decode(_ xml.EndElement) StreamClosePacket {
|
||||||
|
return StreamClosePacket{}
|
||||||
|
}
|
||||||
|
|
|
@ -3,12 +3,19 @@ package stanza
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NSStreamManagement = "urn:xmpp:sm:3"
|
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
|
// Enabled as defined in Stream Management spec
|
||||||
// Reference: https://xmpp.org/extensions/xep-0198.html#enable
|
// Reference: https://xmpp.org/extensions/xep-0198.html#enable
|
||||||
type SMEnabled struct {
|
type SMEnabled struct {
|
||||||
|
@ -23,6 +30,112 @@ func (SMEnabled) Name() string {
|
||||||
return "Stream Management: enabled"
|
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
|
// Request as defined in Stream Management spec
|
||||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||||
type SMRequest struct {
|
type SMRequest struct {
|
||||||
|
@ -37,7 +150,7 @@ func (SMRequest) Name() string {
|
||||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||||
type SMAnswer struct {
|
type SMAnswer struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 a"`
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 a"`
|
||||||
H uint `xml:"h,attr,omitempty"`
|
H uint `xml:"h,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (SMAnswer) Name() string {
|
func (SMAnswer) Name() string {
|
||||||
|
@ -49,24 +162,175 @@ func (SMAnswer) Name() string {
|
||||||
type SMResumed struct {
|
type SMResumed struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"`
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"`
|
||||||
PrevId string `xml:"previd,attr,omitempty"`
|
PrevId string `xml:"previd,attr,omitempty"`
|
||||||
H uint `xml:"h,attr,omitempty"`
|
H *uint `xml:"h,attr,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (SMResumed) Name() string {
|
func (SMResumed) Name() string {
|
||||||
return "Stream Management: resumed"
|
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
|
// Failed as defined in Stream Management spec
|
||||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||||
type SMFailed struct {
|
type SMFailed struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 failed"`
|
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 {
|
func (SMFailed) Name() string {
|
||||||
return "Stream Management: failed"
|
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{}
|
type smDecoder struct{}
|
||||||
|
|
||||||
var sm smDecoder
|
var sm smDecoder
|
||||||
|
@ -78,9 +342,11 @@ func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||||
return s.decodeEnabled(p, se)
|
return s.decodeEnabled(p, se)
|
||||||
case "resumed":
|
case "resumed":
|
||||||
return s.decodeResumed(p, se)
|
return s.decodeResumed(p, se)
|
||||||
|
case "resume":
|
||||||
|
return s.decodeResume(p, se)
|
||||||
case "r":
|
case "r":
|
||||||
return s.decodeRequest(p, se)
|
return s.decodeRequest(p, se)
|
||||||
case "h":
|
case "a":
|
||||||
return s.decodeAnswer(p, se)
|
return s.decodeAnswer(p, se)
|
||||||
case "failed":
|
case "failed":
|
||||||
return s.decodeFailed(p, se)
|
return s.decodeFailed(p, se)
|
||||||
|
@ -102,6 +368,11 @@ func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed,
|
||||||
return packet, err
|
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) {
|
func (smDecoder) decodeRequest(p *xml.Decoder, se xml.StartElement) (SMRequest, error) {
|
||||||
var packet SMRequest
|
var packet SMRequest
|
||||||
err := p.DecodeElement(&packet, &se)
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
|
226
stanza/stream_management_test.go
Normal file
226
stanza/stream_management_test.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPopEmptyQueue(t *testing.T) {
|
||||||
|
var uaq stanza.UnAckQueue
|
||||||
|
popped := uaq.Pop()
|
||||||
|
if popped != nil {
|
||||||
|
t.Fatalf("queue is empty but something was popped !")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushUnack(t *testing.T) {
|
||||||
|
uaq := initUnAckQueue()
|
||||||
|
toPush := stanza.UnAckedStz{
|
||||||
|
Id: 3,
|
||||||
|
Stz: `<iq type='submit'
|
||||||
|
from='confucius@scholars.lit/home'
|
||||||
|
to='registrar.scholars.lit'
|
||||||
|
id='kj3b157n'
|
||||||
|
xml:lang='en'>
|
||||||
|
<query xmlns='jabber:iq:register'>
|
||||||
|
<username>confucius</username>
|
||||||
|
<first>Qui</first>
|
||||||
|
<last>Kong</last>
|
||||||
|
</query>
|
||||||
|
</iq>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := uaq.Push(&toPush)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not push element to the queue : %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uaq.Uslice) != 4 {
|
||||||
|
t.Fatalf("push to the non-acked queue failed")
|
||||||
|
}
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
if uaq.Uslice[i].Id != i+1 {
|
||||||
|
t.Fatalf("indexes were not updated correctly. Expected %d got %d", i, uaq.Uslice[i].Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the queue is a fifo : popped element should not be the one we just pushed.
|
||||||
|
popped := uaq.Pop()
|
||||||
|
poppedElt, ok := popped.(*stanza.UnAckedStz)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("popped element is not a *stanza.UnAckedStz")
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.DeepEqual(*poppedElt, toPush) {
|
||||||
|
t.Fatalf("pushed element is at the top of the fifo queue when it should be at the bottom")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeekUnack(t *testing.T) {
|
||||||
|
uaq := initUnAckQueue()
|
||||||
|
|
||||||
|
expectedPeek := stanza.UnAckedStz{
|
||||||
|
Id: 1,
|
||||||
|
Stz: `<iq type='set'
|
||||||
|
from='romeo@montague.net/home'
|
||||||
|
to='characters.shakespeare.lit'
|
||||||
|
id='search2'
|
||||||
|
xml:lang='en'>
|
||||||
|
<query xmlns='jabber:iq:search'>
|
||||||
|
<last>Capulet</last>
|
||||||
|
</query>
|
||||||
|
</iq>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(expectedPeek, *uaq.Uslice[0]) {
|
||||||
|
t.Fatalf("peek failed to return the correct stanza")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeekNUnack(t *testing.T) {
|
||||||
|
uaq := initUnAckQueue()
|
||||||
|
initLen := len(uaq.Uslice)
|
||||||
|
randPop := rand.Int31n(int32(initLen))
|
||||||
|
|
||||||
|
peeked := uaq.PeekN(int(randPop))
|
||||||
|
|
||||||
|
if len(uaq.Uslice) != initLen {
|
||||||
|
t.Fatalf("queue length changed whith peek n operation : had %d found %d after peek", initLen, len(uaq.Uslice))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peeked) != int(randPop) {
|
||||||
|
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", randPop, len(peeked))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeekNUnackTooLong(t *testing.T) {
|
||||||
|
uaq := initUnAckQueue()
|
||||||
|
initLen := len(uaq.Uslice)
|
||||||
|
|
||||||
|
// Have a random number of elements to peek that's greater than the queue size
|
||||||
|
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
|
||||||
|
|
||||||
|
peeked := uaq.PeekN(int(randPop))
|
||||||
|
|
||||||
|
if len(uaq.Uslice) != initLen {
|
||||||
|
t.Fatalf("total length changed whith peek n operation : had %d found %d after pop", initLen, len(uaq.Uslice))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peeked) != initLen {
|
||||||
|
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", initLen, len(peeked))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPopNUnack(t *testing.T) {
|
||||||
|
uaq := initUnAckQueue()
|
||||||
|
initLen := len(uaq.Uslice)
|
||||||
|
randPop := rand.Int31n(int32(initLen))
|
||||||
|
|
||||||
|
popped := uaq.PopN(int(randPop))
|
||||||
|
|
||||||
|
if len(uaq.Uslice)+len(popped) != initLen {
|
||||||
|
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, elt := range popped {
|
||||||
|
for _, oldElt := range uaq.Uslice {
|
||||||
|
if reflect.DeepEqual(elt, oldElt) {
|
||||||
|
t.Fatalf("pop n operation duplicated some elements")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPopNUnackTooLong(t *testing.T) {
|
||||||
|
uaq := initUnAckQueue()
|
||||||
|
initLen := len(uaq.Uslice)
|
||||||
|
|
||||||
|
// Have a random number of elements to pop that's greater than the queue size
|
||||||
|
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
|
||||||
|
|
||||||
|
popped := uaq.PopN(int(randPop))
|
||||||
|
|
||||||
|
if len(uaq.Uslice)+len(popped) != initLen {
|
||||||
|
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, elt := range popped {
|
||||||
|
for _, oldElt := range uaq.Uslice {
|
||||||
|
if reflect.DeepEqual(elt, oldElt) {
|
||||||
|
t.Fatalf("pop n operation duplicated some elements")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPopUnack(t *testing.T) {
|
||||||
|
uaq := initUnAckQueue()
|
||||||
|
initLen := len(uaq.Uslice)
|
||||||
|
|
||||||
|
popped := uaq.Pop()
|
||||||
|
|
||||||
|
if len(uaq.Uslice)+1 != initLen {
|
||||||
|
t.Fatalf("total length changed whith pop operation : had %d found %d after pop", initLen, len(uaq.Uslice)+1)
|
||||||
|
}
|
||||||
|
for _, oldElt := range uaq.Uslice {
|
||||||
|
if reflect.DeepEqual(popped, oldElt) {
|
||||||
|
t.Fatalf("pop n operation duplicated some elements")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func initUnAckQueue() stanza.UnAckQueue {
|
||||||
|
q := []*stanza.UnAckedStz{
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
Stz: `<iq type='set'
|
||||||
|
from='romeo@montague.net/home'
|
||||||
|
to='characters.shakespeare.lit'
|
||||||
|
id='search2'
|
||||||
|
xml:lang='en'>
|
||||||
|
<query xmlns='jabber:iq:search'>
|
||||||
|
<last>Capulet</last>
|
||||||
|
</query>
|
||||||
|
</iq>`,
|
||||||
|
},
|
||||||
|
{Id: 2,
|
||||||
|
Stz: `<iq type='get'
|
||||||
|
from='juliet@capulet.com/balcony'
|
||||||
|
to='characters.shakespeare.lit'
|
||||||
|
id='search3'
|
||||||
|
xml:lang='en'>
|
||||||
|
<query xmlns='jabber:iq:search'/>
|
||||||
|
</iq>`},
|
||||||
|
{Id: 3,
|
||||||
|
Stz: `<iq type='set'
|
||||||
|
from='juliet@capulet.com/balcony'
|
||||||
|
to='characters.shakespeare.lit'
|
||||||
|
id='search4'
|
||||||
|
xml:lang='en'>
|
||||||
|
<query xmlns='jabber:iq:search'>
|
||||||
|
<x xmlns='jabber:x:data' type='submit'>
|
||||||
|
<field type='hidden' var='FORM_TYPE'>
|
||||||
|
<value>jabber:iq:search</value>
|
||||||
|
</field>
|
||||||
|
<field var='x-gender'>
|
||||||
|
<value>male</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</query>
|
||||||
|
</iq>`},
|
||||||
|
}
|
||||||
|
|
||||||
|
return stanza.UnAckQueue{Uslice: q}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
|
}
|
|
@ -2,16 +2,21 @@ package stanza_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var reLeadcloseWhtsp = regexp.MustCompile(`^[\s\p{Zs}]+|[\s\p{Zs}]+$`)
|
||||||
|
var reInsideWhtsp = regexp.MustCompile(`[\s\p{Zs}]`)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Marshaller / unmarshaller test
|
// 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
|
// Marshall
|
||||||
data, err := xml.Marshal(iq)
|
data, err := xml.Marshal(iq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -63,3 +68,14 @@ func xmlOpts() cmp.Options {
|
||||||
}
|
}
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func delSpaces(s string) string {
|
||||||
|
return reInsideWhtsp.ReplaceAllString(reLeadcloseWhtsp.ReplaceAllString(s, ""), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareMarshal(expected, data string) error {
|
||||||
|
if delSpaces(expected) != delSpaces(data) {
|
||||||
|
return errors.New("failed to verify unmarshal->marshal. Expected :" + expected + "\ngot: " + data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -25,11 +25,11 @@ import (
|
||||||
// set callback and trigger reconnection.
|
// set callback and trigger reconnection.
|
||||||
type StreamClient interface {
|
type StreamClient interface {
|
||||||
Connect() error
|
Connect() error
|
||||||
Resume(state SMState) error
|
Resume() error
|
||||||
Send(packet stanza.Packet) 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
|
SendRaw(packet string) error
|
||||||
Disconnect()
|
Disconnect() error
|
||||||
SetHandler(handler EventHandler)
|
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
|
// It is mostly use in callback to pass a limited subset of the stream client interface
|
||||||
type Sender interface {
|
type Sender interface {
|
||||||
Send(packet stanza.Packet) 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
|
SendRaw(packet string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,22 +74,24 @@ func (sm *StreamManager) Run() error {
|
||||||
return errors.New("missing stream client")
|
return errors.New("missing stream client")
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := func(e Event) {
|
handler := func(e Event) error {
|
||||||
switch e.State {
|
switch e.State.state {
|
||||||
case StateConnected:
|
|
||||||
sm.Metrics.setConnectTime()
|
|
||||||
case StateSessionEstablished:
|
case StateSessionEstablished:
|
||||||
sm.Metrics.setLoginTime()
|
sm.Metrics.setLoginTime()
|
||||||
case StateDisconnected:
|
case StateDisconnected:
|
||||||
// Reconnect on disconnection
|
// Reconnect on disconnection
|
||||||
sm.resume(e.SMState)
|
return sm.resume()
|
||||||
case StateStreamError:
|
case StateStreamError:
|
||||||
sm.client.Disconnect()
|
sm.client.Disconnect()
|
||||||
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
|
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
|
||||||
|
// TODO: Make this conflict exception a permanent error
|
||||||
if e.StreamError != "conflict" {
|
if e.StreamError != "conflict" {
|
||||||
sm.connect()
|
return sm.resume()
|
||||||
}
|
}
|
||||||
|
case StatePermanentError:
|
||||||
|
// Do not attempt to reconnect
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
sm.client.SetHandler(handler)
|
sm.client.SetHandler(handler)
|
||||||
|
|
||||||
|
@ -111,20 +113,33 @@ func (sm *StreamManager) Stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *StreamManager) connect() error {
|
func (sm *StreamManager) connect() error {
|
||||||
var state SMState
|
if sm.client != nil {
|
||||||
return sm.resume(state)
|
if c, ok := sm.client.(*Client); ok {
|
||||||
|
if c.CurrentState.getState() == StateDisconnected {
|
||||||
|
sm.Metrics = initMetrics()
|
||||||
|
err := c.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sm.PostConnect != nil {
|
||||||
|
sm.PostConnect(sm.client)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("client is not disconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
// resume manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
// resume manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
||||||
func (sm *StreamManager) resume(state SMState) error {
|
func (sm *StreamManager) resume() error {
|
||||||
var backoff backoff // TODO: Group backoff calculation features with connection manager?
|
var backoff backoff // TODO: Group backoff calculation features with connection manager?
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var err error
|
var err error
|
||||||
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
||||||
sm.Metrics = initMetrics()
|
sm.Metrics = initMetrics()
|
||||||
|
if err = sm.client.Resume(); err != nil {
|
||||||
if err = sm.client.Resume(state); err != nil {
|
|
||||||
var actualErr ConnError
|
var actualErr ConnError
|
||||||
if xerrors.As(err, &actualErr) {
|
if xerrors.As(err, &actualErr) {
|
||||||
if actualErr.Permanent {
|
if actualErr.Permanent {
|
||||||
|
@ -148,11 +163,6 @@ func (sm *StreamManager) resume(state SMState) error {
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
startTime time.Time
|
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
|
// LoginTime returns the between client initiation of the TCP/IP
|
||||||
// connection to the server and the return of the login result.
|
// connection to the server and the return of the login result.
|
||||||
// This includes ConnectTime, but also XMPP level protocol negotiation
|
// This includes ConnectTime, but also XMPP level protocol negotiation
|
||||||
|
@ -168,10 +178,6 @@ func initMetrics() *Metrics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metrics) setConnectTime() {
|
|
||||||
m.ConnectTime = time.Since(m.startTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Metrics) setLoginTime() {
|
func (m *Metrics) setLoginTime() {
|
||||||
m.LoginTime = time.Since(m.startTime)
|
m.LoginTime = time.Since(m.startTime)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,65 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// TCP Server Mock
|
// TCP Server Mock
|
||||||
|
const (
|
||||||
|
defaultTimeout = 2 * time.Second
|
||||||
|
testComponentDomain = "localhost"
|
||||||
|
defaultServerName = "testServer"
|
||||||
|
defaultStreamID = "91bd0bba-012f-4d92-bb17-5fc41e6fe545"
|
||||||
|
defaultComponentName = "Test Component"
|
||||||
|
serverStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' id='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
||||||
|
|
||||||
|
// Default port is not standard XMPP port to avoid interfering
|
||||||
|
// with local running XMPP server
|
||||||
|
|
||||||
|
// Component tests
|
||||||
|
testHandshakePort = iota + 15222
|
||||||
|
testDecoderPort
|
||||||
|
testSendIqPort
|
||||||
|
testSendIqFailPort
|
||||||
|
testSendRawPort
|
||||||
|
testDisconnectPort
|
||||||
|
testSManDisconnectPort
|
||||||
|
|
||||||
|
// Client tests
|
||||||
|
testClientBasePort
|
||||||
|
testClientRawPort
|
||||||
|
testClientIqPort
|
||||||
|
testClientIqFailPort
|
||||||
|
testClientPostConnectHook
|
||||||
|
|
||||||
|
// Client internal tests
|
||||||
|
testClientStreamManagement
|
||||||
|
)
|
||||||
|
|
||||||
// ClientHandler is passed by the test client to provide custom behaviour to
|
// ClientHandler is passed by the test client to provide custom behaviour to
|
||||||
// the TCP server mock. This allows customizing the server behaviour to allow
|
// the TCP server mock. This allows customizing the server behaviour to allow
|
||||||
// testing clients under various scenarii.
|
// testing clients under various scenarii.
|
||||||
type ClientHandler func(t *testing.T, conn net.Conn)
|
type ClientHandler func(t *testing.T, serverConn *ServerConn)
|
||||||
|
|
||||||
// ServerMock is a simple TCP server that can be use to mock basic server
|
// ServerMock is a simple TCP server that can be use to mock basic server
|
||||||
// behaviour to test clients.
|
// behaviour to test clients.
|
||||||
type ServerMock struct {
|
type ServerMock struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
handler ClientHandler
|
handler ClientHandler
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
connections []net.Conn
|
serverConnections []*ServerConn
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConn struct {
|
||||||
|
connection net.Conn
|
||||||
|
decoder *xml.Decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start launches the mock TCP server, listening to an actual address / port.
|
// Start launches the mock TCP server, listening to an actual address / port.
|
||||||
|
@ -38,9 +77,9 @@ func (mock *ServerMock) Stop() {
|
||||||
if mock.listener != nil {
|
if mock.listener != nil {
|
||||||
mock.listener.Close()
|
mock.listener.Close()
|
||||||
}
|
}
|
||||||
// Close all existing connections
|
// Close all existing serverConnections
|
||||||
for _, c := range mock.connections {
|
for _, c := range mock.serverConnections {
|
||||||
c.Close()
|
c.connection.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,13 +99,14 @@ func (mock *ServerMock) init(addr string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop accepts connections and creates a go routine per connection.
|
// loop accepts serverConnections and creates a go routine per connection.
|
||||||
// The go routine is running the client handler, that is used to provide the
|
// The go routine is running the client handler, that is used to provide the
|
||||||
// real TCP server behaviour.
|
// real TCP server behaviour.
|
||||||
func (mock *ServerMock) loop() {
|
func (mock *ServerMock) loop() {
|
||||||
listener := mock.listener
|
listener := mock.listener
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
|
serverConn := &ServerConn{conn, xml.NewDecoder(conn)}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
case <-mock.done:
|
case <-mock.done:
|
||||||
|
@ -76,8 +116,204 @@ func (mock *ServerMock) loop() {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mock.connections = append(mock.connections, conn)
|
mock.serverConnections = append(mock.serverConnections, serverConn)
|
||||||
|
|
||||||
// TODO Create and pass a context to cancel the handler if they are still around = avoid possible leak on complex handlers
|
// TODO Create and pass a context to cancel the handler if they are still around = avoid possible leak on complex handlers
|
||||||
go mock.handler(mock.t, conn)
|
go mock.handler(mock.t, serverConn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//======================================================================================================================
|
||||||
|
// A few functions commonly used for tests. Trying to avoid duplicates in client and component test files.
|
||||||
|
//======================================================================================================================
|
||||||
|
|
||||||
|
func respondToIQ(t *testing.T, sc *ServerConn) {
|
||||||
|
// Decoder to parse the request
|
||||||
|
iqReq, err := receiveIq(sc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to receive IQ : %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if vld, _ := iqReq.IsValid(); !vld {
|
||||||
|
mockIQError(sc.connection)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crafting response
|
||||||
|
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`)
|
||||||
|
|
||||||
|
disco.AddIdentity("Multicast", "service", "multicast")
|
||||||
|
iqResp.Payload = disco
|
||||||
|
|
||||||
|
// Sending response to the Component
|
||||||
|
mResp, err := xml.Marshal(iqResp)
|
||||||
|
_, err = fmt.Fprintln(sc.connection, string(mResp))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not send response stanza : %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
t.Errorf("read timeout: %s", err)
|
||||||
|
} else {
|
||||||
|
t.Errorf("read error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = xml.Unmarshal(recvBuf, &presenceStz)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected presence but this happened : %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads next request coming from the Component. Expecting it to be an IQ request
|
||||||
|
func receiveIq(sc *ServerConn) (*stanza.IQ, error) {
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &iqStz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be used in server handlers when an IQ sent by a client or component is invalid.
|
||||||
|
// This responds as expected from a "real" server, aside from the error message.
|
||||||
|
func mockIQError(c net.Conn) {
|
||||||
|
s := stanza.StreamError{
|
||||||
|
XMLName: xml.Name{Local: "stream:error"},
|
||||||
|
Error: xml.Name{Local: "xml-not-well-formed"},
|
||||||
|
Text: `XML was not well-formed`,
|
||||||
|
}
|
||||||
|
raw, _ := xml.Marshal(s)
|
||||||
|
fmt.Fprintln(c, string(raw))
|
||||||
|
fmt.Fprintln(c, `</stream:stream>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendStreamFeatures(t *testing.T, sc *ServerConn) {
|
||||||
|
// This is a basic server, supporting only 1 stream feature: SASL Plain Auth
|
||||||
|
features := `<stream:features>
|
||||||
|
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
||||||
|
<mechanism>PLAIN</mechanism>
|
||||||
|
</mechanisms>
|
||||||
|
</stream:features>`
|
||||||
|
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||||
|
t.Errorf("cannot send stream feature: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO return err in case of error reading the auth params
|
||||||
|
func readAuth(t *testing.T, decoder *xml.Decoder) string {
|
||||||
|
se, err := stanza.NextStart(decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read auth: %s", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var nv interface{}
|
||||||
|
nv = &stanza.SASLAuth{}
|
||||||
|
// Decode element into pointer storage
|
||||||
|
if err = decoder.DecodeElement(nv, &se); err != nil {
|
||||||
|
t.Errorf("cannot decode auth: %s", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := nv.(type) {
|
||||||
|
case *stanza.SASLAuth:
|
||||||
|
return v.Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendBindFeature(t *testing.T, sc *ServerConn) {
|
||||||
|
// This is a basic server, supporting only 1 stream feature after auth: resource binding
|
||||||
|
features := `<stream:features>
|
||||||
|
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||||
|
</stream:features>`
|
||||||
|
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||||
|
t.Errorf("cannot send stream feature: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendRFC3921Feature(t *testing.T, sc *ServerConn) {
|
||||||
|
// This is a basic server, supporting only 2 features after auth: resource & session binding
|
||||||
|
features := `<stream:features>
|
||||||
|
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||||
|
<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
|
||||||
|
</stream:features>`
|
||||||
|
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||||
|
t.Errorf("cannot send stream feature: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bind(t *testing.T, sc *ServerConn) {
|
||||||
|
se, err := stanza.NextStart(sc.decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read bind: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iq := &stanza.IQ{}
|
||||||
|
// Decode element into pointer storage
|
||||||
|
if err = sc.decoder.DecodeElement(&iq, &se); err != nil {
|
||||||
|
t.Errorf("cannot decode bind iq: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Check all elements
|
||||||
|
switch iq.Payload.(type) {
|
||||||
|
case *stanza.Bind:
|
||||||
|
result := `<iq id='%s' type='result'>
|
||||||
|
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
|
||||||
|
<jid>%s</jid>
|
||||||
|
</bind>
|
||||||
|
</iq>`
|
||||||
|
fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real Jid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(t *testing.T, sc *ServerConn) {
|
||||||
|
se, err := stanza.NextStart(sc.decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read session: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iq := &stanza.IQ{}
|
||||||
|
// Decode element into pointer storage
|
||||||
|
if err = sc.decoder.DecodeElement(&iq, &se); err != nil {
|
||||||
|
t.Errorf("cannot decode session iq: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch iq.Payload.(type) {
|
||||||
|
case *stanza.StreamSession:
|
||||||
|
result := `<iq id='%s' type='result'/>`
|
||||||
|
fmt.Fprintf(sc.connection, result, iq.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue