2 Commits

Author SHA1 Message Date
Mickael Remond
81f89c536d Update xerrors for go 1.13 2019-09-27 17:24:34 +02:00
Mickael Remond
1f2cf8d772 We do not need the Content to be innerxml. cdata is enough.
Fixes #110
2019-09-27 17:23:38 +02:00
84 changed files with 684 additions and 7568 deletions

View File

@@ -1,38 +0,0 @@
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

View File

@@ -1,42 +0,0 @@
# Fluux XMPP Changelog
## 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
### Changes
- XMPP Over Websocket support
- Add support for getting IQ responses to client IQ queries (synchronously or asynchronously, passing an handler
function).
- Implement X-OAUTH2 authentication method. You can read more details here:
[Understanding ejabberd OAuth Support & Roadmap: Step 4](https://blog.process-one.net/understanding-ejabberd-oauth-support-roadmap/)
- Fix issues in the stanza builder when trying to add text inside and XMPP node.
- Fix issues with unescaped % characters in XMPP payload.
### Code migration guide
TODO

4
Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM golang:1.12
WORKDIR /xmpp
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
COPY . ./

View File

@@ -1,6 +1,6 @@
# Fluux XMPP # Fluux 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) [![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)
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.
@@ -34,8 +34,8 @@ Here is an example code to configure a client to allow connecting to a server wi
config := xmpp.Config{ config := xmpp.Config{
Address: "localhost:5222", Address: "localhost:5222",
Jid: "test@localhost", Jid: "test@localhost",
Credential: xmpp.Password("Test"), Password: "test",
TLSConfig: tls.Config{InsecureSkipVerify: true}, TLSConfig: tls.Config{InsecureSkipVerify: true},
} }
``` ```
@@ -52,16 +52,7 @@ 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 ## Stanza subpackage
- [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
### Stanza subpackage
XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to
leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas
@@ -82,14 +73,6 @@ implement your own extensions directly in your own application.
To learn more about the stanza package, you can read more in the To learn more about the stanza package, you can read more in the
[stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md). [stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md).
### Router
TODO
### Getting IQ response from server
TODO
## Examples ## Examples
We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using
@@ -111,20 +94,17 @@ import (
func main() { func main() {
config := xmpp.Config{ config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{ Address: "localhost:5222",
Address: "localhost:5222",
},
Jid: "test@localhost", Jid: "test@localhost",
Credential: xmpp.Password("test"), 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, errorHandler) client, err := xmpp.NewClient(config, router)
if err != nil { if err != nil {
log.Fatalf("%+v", err) log.Fatalf("%+v", err)
} }
@@ -146,11 +126,6 @@ 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

View File

@@ -11,12 +11,9 @@ import (
func main() { func main() {
opts := xmpp.ComponentOptions{ opts := xmpp.ComponentOptions{
TransportConfiguration: xmpp.TransportConfiguration{ Domain: "service.localhost",
Address: "localhost:9999", Secret: "mypass",
Domain: "service.localhost", Address: "localhost:9999",
},
Domain: "service.localhost",
Secret: "mypass",
// TODO: Move that part to a component discovery handler // TODO: Move that part to a component discovery handler
Name: "Test Component", Name: "Test Component",
@@ -171,7 +168,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
return return
} }
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSubGeneric) pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub)
if !ok { if !ok {
// We only support pubsub delegation // We only support pubsub delegation
return return
@@ -180,7 +177,7 @@ 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 := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
payload := stanza.PubSubGeneric{ payload := stanza.PubSub{
XMLName: xml.Name{ XMLName: xml.Name{
Space: "http://jabber.org/protocol/pubsub", Space: "http://jabber.org/protocol/pubsub",
Local: "pubsub", Local: "pubsub",

View File

@@ -1,6 +1,6 @@
module gosrc.io/xmpp/_examples module gosrc.io/xmpp/_examples
go 1.13 go 1.12
require ( require (
github.com/processone/mpg123 v1.0.0 github.com/processone/mpg123 v1.0.0

View File

@@ -1,211 +1,7 @@
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/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-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-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.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.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/fatih/color v1.6.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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
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/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
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.3.1/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.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/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
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/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/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/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/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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/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.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
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.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/processone/mpg123 v1.0.0/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/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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/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.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
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.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-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/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/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-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-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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/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/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-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-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-20190422165155-953cdadca894/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-20190813064441-fde4db37ae7a/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/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 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/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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/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/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=

View File

@@ -1,3 +0,0 @@
# 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.

View File

@@ -1,51 +0,0 @@
# 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

View File

@@ -1,13 +0,0 @@
# 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"

View File

@@ -1,10 +0,0 @@
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.3.1-0.20191223080939-f8f820170e08
)

View File

@@ -1,371 +0,0 @@
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
}

View File

@@ -1,339 +0,0 @@
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(g, 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(g *gocui.Gui, 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
}

View File

@@ -10,12 +10,9 @@ import (
func main() { func main() {
opts := xmpp.ComponentOptions{ opts := xmpp.ComponentOptions{
TransportConfiguration: xmpp.TransportConfiguration{
Address: "localhost:8888",
Domain: "service2.localhost",
},
Domain: "service2.localhost", Domain: "service2.localhost",
Secret: "mypass", Secret: "mypass",
Address: "localhost:8888",
Name: "Test Component", Name: "Test Component",
Category: "gateway", Category: "gateway",
Type: "service", Type: "service",
@@ -35,7 +32,7 @@ func main() {
IQNamespaces("jabber:iq:version"). IQNamespaces("jabber:iq:version").
HandlerFunc(handleVersion) HandlerFunc(handleVersion)
component, err := xmpp.NewComponent(opts, router, handleError) component, err := xmpp.NewComponent(opts, router)
if err != nil { if err != nil {
log.Fatalf("%+v", err) log.Fatalf("%+v", err)
} }
@@ -47,10 +44,6 @@ 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 {
@@ -62,7 +55,7 @@ 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 != stanza.IQTypeGet { if !ok || iq.Type != "get" {
return return
} }
@@ -77,7 +70,7 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
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 != stanza.IQTypeGet { if !ok || iq.Type != "get" {
return return
} }

View File

@@ -1,4 +0,0 @@
# 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.

View File

@@ -1,75 +0,0 @@
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"
"time"
xmpp "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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet,
From: domain,
To: "localhost",
Id: "my-iq1"})
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")
}
}

View File

@@ -15,11 +15,9 @@ import (
func main() { func main() {
config := xmpp.Config{ config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{ Address: "localhost:5222",
Address: "localhost:5222",
},
Jid: "test@localhost", Jid: "test@localhost",
Credential: xmpp.Password("test"), Password: "test",
StreamLogger: os.Stdout, StreamLogger: os.Stdout,
Insecure: true, Insecure: true,
// TLSConfig: tls.Config{InsecureSkipVerify: true}, // TLSConfig: tls.Config{InsecureSkipVerify: true},
@@ -28,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, errorHandler) client, err := xmpp.NewClient(config, router)
if err != nil { if err != nil {
log.Fatalf("%+v", err) log.Fatalf("%+v", err)
} }
@@ -51,6 +49,5 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
_ = s.Send(reply) _ = s.Send(reply)
} }
func errorHandler(err error) { // TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
fmt.Println(err.Error()) // (using templates ?)
}

View File

@@ -1,37 +0,0 @@
# 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>
```

View File

@@ -3,7 +3,6 @@
package main package main
import ( import (
"encoding/xml"
"flag" "flag"
"fmt" "fmt"
"log" "log"
@@ -20,7 +19,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()
@@ -33,11 +32,9 @@ func main() {
// 2. Prepare XMPP client // 2. Prepare XMPP client
config := xmpp.Config{ config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{ Address: *address,
Address: *address, Jid: *jid,
}, Password: *password,
Jid: *jid,
Credential: xmpp.Password(*password),
// StreamLogger: os.Stdout, // StreamLogger: os.Stdout,
Insecure: true, Insecure: true,
} }
@@ -49,12 +46,12 @@ func main() {
handleMessage(s, p, player) handleMessage(s, p, player)
}) })
router.NewRoute(). router.NewRoute().
Packet("iq"). Packet("message").
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, errorHandler) client, err := xmpp.NewClient(config, router)
if err != nil { if err != nil {
log.Fatalf("%+v", err) log.Fatalf("%+v", err)
} }
@@ -62,9 +59,6 @@ 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)
@@ -109,29 +103,11 @@ 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) {
rq, err := stanza.NewPublishItemRq("localhost", tune := stanza.Tune{Artist: artist, Title: title}
"http://jabber.org/protocol/tune", iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
"", payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
stanza.Item{ iq.Payload = &payload
XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"}, _ = s.Send(iq)
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) {
@@ -139,7 +115,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(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID)) _ = p.Play(url)
} }
// TODO // TODO

View File

@@ -1,54 +0,0 @@
/*
xmpp_oauth2 is a demo client that connect on an XMPP server using OAuth2 and prints received messages.
*/
package main
import (
"fmt"
"log"
"os"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: "localhost:5222",
// TLSConfig: tls.Config{InsecureSkipVerify: true},
},
Jid: "test@localhost",
Credential: xmpp.OAuthToken("OdAIsBlY83SLBaqQoClAn7vrZSHxixT8"),
StreamLogger: os.Stdout,
// Insecure: true,
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
client, err := xmpp.NewClient(config, router, errorHandler)
if err != nil {
log.Fatalf("%+v", err)
}
// If you pass the client to a connection manager, it will handle the reconnect policy
// for you automatically.
cm := xmpp.NewStreamManager(client, nil)
log.Fatal(cm.Run())
}
func errorHandler(err error) {
fmt.Println(err.Error())
}
func handleMessage(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
return
}
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
}

View File

@@ -1,52 +0,0 @@
/*
xmpp_websocket is a demo client that connect on an XMPP server using websocket and prints received messages.ß
*/
package main
import (
"fmt"
"log"
"os"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: "wss://localhost:5443/ws",
},
Jid: "test@localhost",
Credential: xmpp.Password("test"),
StreamLogger: os.Stdout,
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
client, err := xmpp.NewClient(config, router, errorHandler)
if err != nil {
log.Fatalf("%+v", err)
}
// If you pass the client to a connection manager, it will handle the reconnect policy
// for you automatically.
cm := xmpp.NewStreamManager(client, nil)
log.Fatal(cm.Run())
}
func errorHandler(err error) {
fmt.Println(err.Error())
}
func handleMessage(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
return
}
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
}

67
auth.go
View File

@@ -10,60 +10,29 @@ import (
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
// Credential is used to pass the type of secret that will be used to connect to XMPP server. func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
// It can be either a password or an OAuth 2 bearer token. // TODO: Implement other type of SASL Authentication
type Credential struct { havePlain := false
secret string for _, m := range f.Mechanisms.Mechanism {
mechanisms []string if m == "PLAIN" {
} havePlain = true
func Password(pwd string) Credential {
credential := Credential{
secret: pwd,
mechanisms: []string{"PLAIN"},
}
return credential
}
func OAuthToken(token string) Credential {
credential := Credential{
secret: token,
mechanisms: []string{"X-OAUTH2"},
}
return credential
}
// ============================================================================
// Authentication flow for SASL mechanisms
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, credential Credential) (err error) {
var matchingMech string
for _, mech := range credential.mechanisms {
if isSupportedMech(mech, f.Mechanisms.Mechanism) {
matchingMech = mech
break break
} }
} }
if !havePlain {
switch matchingMech { err := fmt.Errorf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism)
case "PLAIN", "X-OAUTH2":
// TODO: Implement other type of SASL mechanisms
return authPlain(socket, decoder, matchingMech, user, credential.secret)
default:
err := fmt.Errorf("no matching authentication (%v) supported by server: %v", credential.mechanisms, f.Mechanisms.Mechanism)
return NewConnError(err, true) return NewConnError(err, true)
} }
return authPlain(socket, decoder, user, password)
} }
// Plain authentication: send base64-encoded \x00 user \x00 password // Plain authentication: send base64-encoded \x00 user \x00 password
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user string, secret string) error { func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
raw := "\x00" + user + "\x00" + secret raw := "\x00" + user + "\x00" + password
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))
_, err := fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='%s'>%s</auth>", stanza.NSSASL, mech, enc) fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
if err != nil {
return err
}
// 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)
@@ -82,13 +51,3 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str
} }
return err return err
} }
// isSupportedMech returns true if the mechanism is supported in the provided list.
func isSupportedMech(mech string, mechanisms []string) bool {
for _, m := range mechanisms {
if mech == m {
return true
}
}
return false
}

View File

@@ -51,7 +51,7 @@ func (c *ServerCheck) Check() error {
decoder := xml.NewDecoder(tcpconn) decoder := xml.NewDecoder(tcpconn)
// Send stream open tag // Send stream open tag
if _, err = fmt.Fprintf(tcpconn, clientStreamOpen, c.domain); err != nil { if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); err != nil {
return err return err
} }
@@ -79,10 +79,7 @@ func (c *ServerCheck) Check() error {
} }
if _, ok := f.DoesStartTLS(); ok { if _, ok := f.DoesStartTLS(); ok {
_, err = fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>") 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 {

160
client.go
View File

@@ -1,10 +1,9 @@
package xmpp package xmpp
import ( import (
"context"
"encoding/xml" "encoding/xml"
"errors" "errors"
"io" "fmt"
"net" "net"
"time" "time"
@@ -20,12 +19,10 @@ type ConnState = uint8
// 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 (
InitialPresence = "<presence/>"
StateDisconnected ConnState = iota StateDisconnected ConnState = iota
StateConnected StateConnected
StateSessionEstablished StateSessionEstablished
StateStreamError StateStreamError
StatePermanentError
) )
// 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
@@ -50,7 +47,7 @@ type SMState struct {
// 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) error type EventHandler func(Event)
type EventManager struct { type EventManager struct {
// Store current state // Store current state
@@ -60,21 +57,21 @@ type EventManager struct {
Handler EventHandler Handler EventHandler
} }
func (em *EventManager) updateState(state ConnState) { func (em EventManager) updateState(state ConnState) {
em.CurrentState = state em.CurrentState = 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) { func (em EventManager) disconnected(state SMState) {
em.CurrentState = StateDisconnected em.CurrentState = 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) { func (em EventManager) streamError(error, desc string) {
em.CurrentState = StateStreamError em.CurrentState = 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})
@@ -84,22 +81,19 @@ func (em *EventManager) streamError(error, desc string) {
// Client // Client
// ============================================================================ // ============================================================================
var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ stanzas")
// Client is the main structure used to connect as a client on an XMPP // Client is the main structure used to connect as a client on an XMPP
// 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 // TCP level connection / can be replaced by a TLS session after starttls
conn net.Conn
// Router is used to dispatch packets // Router is used to dispatch packets
router *Router router *Router
// Track and broadcast connection state // Track and broadcast connection state
EventManager EventManager
// Handle errors from client execution
ErrorHandler func(error)
} }
/* /*
@@ -107,20 +101,17 @@ 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 domainpart of the JID.
// Default the port to 5222. // Default the port to 5222.
func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) { func NewClient(config Config, r *Router) (c *Client, err error) {
if config.KeepaliveInterval == 0 { // Parse JID
config.KeepaliveInterval = time.Second * 30 if config.parsedJid, err = NewJid(config.Jid); err != nil {
}
// 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)
} }
if config.Credential.secret == "" { if config.Password == "" {
err = errors.New("missing credential") err = errors.New("missing password")
return nil, NewConnError(err, true) return nil, NewConnError(err, true)
} }
@@ -142,30 +133,16 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e
} }
} }
} }
if config.Domain == "" { config.Address = ensurePort(config.Address, 5222)
// 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
} }
if config.TransportConfiguration.Domain == "" {
config.TransportConfiguration.Domain = config.parsedJid.Domain
}
c.config.TransportConfiguration.ConnectTimeout = c.config.ConnectTimeout
c.transport = NewClientTransport(c.config.TransportConfiguration)
if config.StreamLogger != nil {
c.transport.LogTraffic(config.StreamLogger)
}
return return
} }
@@ -181,40 +158,21 @@ func (c *Client) Connect() error {
func (c *Client) Resume(state SMState) error { func (c *Client) Resume(state SMState) error {
var err error var err error
streamId, err := c.transport.Connect() c.conn, err = net.DialTimeout("tcp", c.config.Address, time.Duration(c.config.ConnectTimeout)*time.Second)
if err != nil { if err != nil {
return err return err
} }
c.updateState(StateConnected) c.updateState(StateConnected)
// Client is ok, we now open XMPP session // Client is ok, we now open XMPP session
if c.Session, err = NewSession(c.transport, c.config, state); err != nil { if c.conn, c.Session, err = NewSession(c.conn, c.config, 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.updateState(StateSessionEstablished) c.updateState(StateSessionEstablished)
// Start the keepalive go routine // Start the keepalive go routine
keepaliveQuit := make(chan struct{}) keepaliveQuit := make(chan struct{})
go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit) go keepalive(c.conn, keepaliveQuit)
// Start the receiver go routine // Start the receiver go routine
state = c.Session.SMState state = c.Session.SMState
go c.recv(state, keepaliveQuit) go c.recv(state, keepaliveQuit)
@@ -223,17 +181,18 @@ func (c *Client) Resume(state SMState) error {
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online") //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 ? // 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 ? // 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)) fmt.Fprintf(c.Session.streamLogger, "<presence/>")
return err return err
} }
func (c *Client) Disconnect() error { func (c *Client) Disconnect() {
if c.transport != nil { _ = c.SendRaw("</stream:stream>")
return c.transport.Close() // TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
conn := c.conn
if conn != nil {
_ = conn.Close()
} }
// No transport so no connection.
return nil
} }
func (c *Client) SetHandler(handler EventHandler) { func (c *Client) SetHandler(handler EventHandler) {
@@ -242,7 +201,7 @@ func (c *Client) SetHandler(handler EventHandler) {
// Send marshals XMPP stanza and sends it to the server. // Send marshals XMPP stanza and sends it to the server.
func (c *Client) Send(packet stanza.Packet) error { func (c *Client) Send(packet stanza.Packet) error {
conn := c.transport conn := c.conn
if conn == nil { if conn == nil {
return errors.New("client is not connected") return errors.New("client is not connected")
} }
@@ -252,26 +211,7 @@ func (c *Client) Send(packet stanza.Packet) error {
return errors.New("cannot marshal packet " + err.Error()) return errors.New("cannot marshal packet " + err.Error())
} }
return c.sendWithWriter(c.transport, data) return c.sendWithLogger(string(data))
}
// 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 context should have a timeout to prevent the client from waiting
// forever for an IQ result. For example:
//
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
// result := <- client.SendIQ(ctx, iq)
//
func (c *Client) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
return nil, ErrCanOnlySendGetOrSetIq
}
if err := c.Send(iq); err != nil {
return nil, err
}
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
} }
// SendRaw sends an XMPP stanza as a string to the server. // SendRaw sends an XMPP stanza as a string to the server.
@@ -279,17 +219,17 @@ func (c *Client) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, erro
// disconnect the client. It is up to the user of this method to // disconnect the client. It is up to the user of this method to
// carefully craft the XML content to produce valid XMPP. // carefully craft the XML content to produce valid XMPP.
func (c *Client) SendRaw(packet string) error { func (c *Client) SendRaw(packet string) error {
conn := c.transport conn := c.conn
if conn == nil { if conn == nil {
return errors.New("client is not connected") return errors.New("client is not connected")
} }
return c.sendWithWriter(c.transport, []byte(packet)) return c.sendWithLogger(packet)
} }
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { func (c *Client) sendWithLogger(packet string) error {
var err error var err error
_, err = writer.Write(packet) _, err = fmt.Fprintf(c.Session.streamLogger, packet)
return err return err
} }
@@ -297,14 +237,13 @@ 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{}) { func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) {
for { for {
val, err := stanza.NextPacket(c.transport.GetDecoder()) val, err := stanza.NextPacket(c.Session.decoder)
if err != nil { if err != nil {
c.ErrorHandler(err)
close(keepaliveQuit) close(keepaliveQuit)
c.disconnected(state) c.disconnected(state)
return return err
} }
// Handle stream errors // Handle stream errors
@@ -313,46 +252,35 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) {
c.router.route(c, val) c.router.route(c, val)
close(keepaliveQuit) close(keepaliveQuit)
c.streamError(packet.Error.Local, packet.Text) c.streamError(packet.Error.Local, packet.Text)
c.ErrorHandler(errors.New("stream error: " + packet.Error.Local)) return 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: state.Inbound}
err = c.Send(answer) 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++ state.Inbound++
} }
// Do normal route processing in a go-routine so we can immediately
// start receiving other stanzas. This also allows route handlers to c.router.route(c, val)
// send and receive more stanzas.
go c.router.route(c, val)
} }
} }
// 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, interval time.Duration, quit <-chan struct{}) { func keepalive(conn net.Conn, quit <-chan struct{}) {
ticker := time.NewTicker(interval) // TODO: Make keepalive interval configurable
ticker := time.NewTicker(30 * time.Second)
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
if err := transport.Ping(); err != nil { if n, err := fmt.Fprintf(conn, "\n"); err != nil || n != 1 {
// When keepalive fails, we force close the transport. In all cases, the recv will also fail. // When keep alive fails, we force close the connection. In all cases, the recv will also fail.
ticker.Stop() ticker.Stop()
_ = transport.Close() _ = conn.Close()
return return
} }
case <-quit: case <-quit:

View File

@@ -1,19 +0,0 @@
package xmpp
import (
"bytes"
"testing"
)
func TestClient_Send(t *testing.T) {
buffer := bytes.NewBufferString("")
client := Client{}
data := []byte("https://da.wikipedia.org/wiki/J%C3%A6vnd%C3%B8gn")
if err := client.sendWithWriter(buffer, data); err != nil {
t.Errorf("Writing failed: %v", err)
}
if buffer.String() != string(data) {
t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String())
}
}

View File

@@ -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,46 +14,23 @@ 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(StateConnected)
if mgr.CurrentState != StateConnected {
t.Fatal("CurrentState not updated by updateState()")
}
mgr.disconnected(SMState{})
if mgr.CurrentState != StateDisconnected {
t.Fatalf("CurrentState not reset by disconnected()")
}
mgr.streamError(ErrTLSNotSupported.Error(), "")
if mgr.CurrentState != 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, handlerClientConnectSuccess) mock.Start(t, testXMPPAddress, handlerConnectSuccess)
// Test / Check result // Test / Check result
config := Config{ config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true}
var client *Client var client *Client
var err error var err error
router := NewRouter() router := NewRouter()
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { if client, err = NewClient(config, router); err != nil {
t.Errorf("connect create XMPP client: %s", err) t.Errorf("connect create XMPP client: %s", err)
} }
@@ -67,24 +44,15 @@ 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, func(t *testing.T, sc *ServerConn) { mock.Start(t, testXMPPAddress, handlerAbortTLS)
handlerAbortTLS(t, sc)
closeConn(t, sc)
})
// Test / Check result // Test / Check result
config := Config{ config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
}
var client *Client var client *Client
var err error var err error
router := NewRouter() router := NewRouter()
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { if client, err = NewClient(config, router); err != nil {
t.Errorf("cannot create XMPP client: %s", err) t.Errorf("cannot create XMPP client: %s", err)
} }
@@ -100,24 +68,15 @@ 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, func(t *testing.T, sc *ServerConn) { mock.Start(t, testXMPPAddress, handlerAbortTLS)
handlerAbortTLS(t, sc)
closeConn(t, sc)
})
// Test / Check result // Test / Check result
config := Config{ config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
}
var client *Client var client *Client
var err error var err error
router := NewRouter() router := NewRouter()
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { if client, err = NewClient(config, router); err != nil {
t.Errorf("cannot create XMPP client: %s", err) t.Errorf("cannot create XMPP client: %s", err)
} }
@@ -132,22 +91,15 @@ 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, handlerClientConnectWithSession) mock.Start(t, testXMPPAddress, handlerConnectWithSession)
// Test / Check result // Test / Check result
config := Config{ config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true,
}
var client *Client var client *Client
var err error var err error
router := NewRouter() router := NewRouter()
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { if client, err = NewClient(config, router); err != nil {
t.Errorf("connect create XMPP client: %s", err) t.Errorf("connect create XMPP client: %s", err)
} }
@@ -158,286 +110,54 @@ 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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
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()
}
//============================================================================= //=============================================================================
// 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 handlerClientConnectSuccess(t *testing.T, sc *ServerConn) { func handlerConnectSuccess(t *testing.T, c net.Conn) {
checkClientOpenStream(t, sc) decoder := xml.NewDecoder(c)
sendStreamFeatures(t, sc) // Send initial features checkOpenStream(t, c, decoder)
readAuth(t, sc.decoder)
fmt.Fprintln(sc.connection, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
checkClientOpenStream(t, sc) // Reset stream sendStreamFeatures(t, c, decoder) // Send initial features
sendBindFeature(t, sc) // Send post auth features readAuth(t, decoder)
bind(t, sc) fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
}
// 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:
fmt.Fprintf(sc.connection, 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, sc *ServerConn) { func handlerAbortTLS(t *testing.T, c net.Conn) {
checkClientOpenStream(t, sc) decoder := xml.NewDecoder(c)
sendStreamFeatures(t, sc) // Send initial features checkOpenStream(t, c, decoder)
sendStreamFeatures(t, c, decoder) // Send initial features
} }
// Test connection with mandatory session (RFC-3921) // Test connection with mandatory session (RFC-3921)
func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) { func handlerConnectWithSession(t *testing.T, c net.Conn) {
checkClientOpenStream(t, sc) decoder := xml.NewDecoder(c)
checkOpenStream(t, c, decoder)
sendStreamFeatures(t, sc) // Send initial features sendStreamFeatures(t, c, decoder) // Send initial features
readAuth(t, sc.decoder) readAuth(t, decoder)
fmt.Fprintln(sc.connection, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>") fmt.Fprintln(c, "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>")
checkClientOpenStream(t, sc) // Reset stream checkOpenStream(t, c, decoder) // Reset stream
sendRFC3921Feature(t, sc) // Send post auth features sendRFC3921Feature(t, c, decoder) // Send post auth features
bind(t, sc) bind(t, c, decoder)
session(t, sc) session(t, c, decoder)
} }
func checkClientOpenStream(t *testing.T, sc *ServerConn) { func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) c.SetDeadline(time.Now().Add(defaultTimeout))
defer sc.connection.SetDeadline(time.Time{}) defer c.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 := sc.decoder.Token() token, err := decoder.Token()
if err != nil { if err != nil {
t.Errorf("cannot read next token: %s", err) t.Errorf("cannot read next token: %s", err)
} }
@@ -449,7 +169,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) {
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(sc.connection, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil { if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
t.Errorf("cannot write server stream open: %s", err) t.Errorf("cannot write server stream open: %s", err)
} }
return return
@@ -457,35 +177,105 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) {
} }
} }
func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int) (*Client, *ServerMock) { func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) {
mock := &ServerMock{} // This is a basic server, supporting only 1 stream feature: SASL Plain Auth
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port) features := `<stream:features>
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
mock.Start(t, testServerAddress, serverHandler) <mechanism>PLAIN</mechanism>
</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
} }
// This really should not be used as is. // TODO return err in case of error reading the auth params
// It's just meant to be a placeholder when error handling is not needed at this level func readAuth(t *testing.T, decoder *xml.Decoder) string {
func clientDefaultErrorHandler(err error) { se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read auth: %s", err)
return ""
}
var nv interface{}
nv = &stanza.SASLAuth{}
// Decode element into pointer storage
if err = decoder.DecodeElement(nv, &se); err != nil {
t.Errorf("cannot decode auth: %s", err)
return ""
}
switch v := nv.(type) {
case *stanza.SASLAuth:
return v.Value
}
return ""
}
func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
// This is a basic server, supporting only 1 stream feature after auth: resource binding
features := `<stream:features>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
</stream:features>`
if _, err := fmt.Fprintln(c, features); err != nil {
t.Errorf("cannot send stream feature: %s", err)
}
}
func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) {
// This is a basic server, supporting only 2 features after auth: resource & session binding
features := `<stream:features>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
</stream:features>`
if _, err := fmt.Fprintln(c, features); err != nil {
t.Errorf("cannot send stream feature: %s", err)
}
}
func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read bind: %s", err)
return
}
iq := &stanza.IQ{}
// Decode element into pointer storage
if err = decoder.DecodeElement(&iq, &se); err != nil {
t.Errorf("cannot decode bind iq: %s", err)
return
}
// TODO Check all elements
switch iq.Payload.(type) {
case *stanza.Bind:
result := `<iq id='%s' type='result'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<jid>%s</jid>
</bind>
</iq>`
fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID
}
}
func session(t *testing.T, c net.Conn, decoder *xml.Decoder) {
se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read session: %s", err)
return
}
iq := &stanza.IQ{}
// Decode element into pointer storage
if err = decoder.DecodeElement(&iq, &se); err != nil {
t.Errorf("cannot decode session iq: %s", err)
return
}
switch iq.Payload.(type) {
case *stanza.StreamSession:
result := `<iq id='%s' type='result'/>`
fmt.Fprintf(c, result, iq.Id)
}
} }

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"bufio" "bufio"
"gosrc.io/xmpp/stanza"
"os" "os"
"strings" "strings"
"sync" "sync"
@@ -33,11 +32,9 @@ func sendxmpp(cmd *cobra.Command, args []string) {
var err error var err error
client, err := xmpp.NewClient(xmpp.Config{ client, err := xmpp.NewClient(xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{ Jid: viper.GetString("jid"),
Address: viper.GetString("addr"), Address: viper.GetString("addr"),
}, Password: viper.GetString("password"),
Jid: viper.GetString("jid"),
Credential: xmpp.Password(viper.GetString("password")),
}, xmpp.NewRouter()) }, xmpp.NewRouter())
if err != nil { if err != nil {
@@ -49,7 +46,7 @@ func sendxmpp(cmd *cobra.Command, args []string) {
wg.Add(1) wg.Add(1)
// FIXME: Remove global variables // FIXME: Remove global variables
var mucsToLeave []*stanza.Jid var mucsToLeave []*xmpp.Jid
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) { cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
defer wg.Done() defer wg.Done()
@@ -58,7 +55,7 @@ func sendxmpp(cmd *cobra.Command, args []string) {
if isMUCRecipient { if isMUCRecipient {
for _, muc := range receiver { for _, muc := range receiver {
jid, err := stanza.NewJid(muc) jid, err := xmpp.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

View File

@@ -7,7 +7,7 @@ import (
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
func joinMUC(c xmpp.Sender, toJID *stanza.Jid) error { func joinMUC(c xmpp.Sender, toJID *xmpp.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 *stanza.Jid) error {
}) })
} }
func leaveMUCs(c xmpp.Sender, mucsToLeave []*stanza.Jid) { func leaveMUCs(c xmpp.Sender, mucsToLeave []*xmpp.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(),

View File

@@ -1,12 +1,12 @@
module gosrc.io/xmpp/cmd module gosrc.io/xmpp/cmd
go 1.13 go 1.12
require ( 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.6.1 github.com/spf13/viper v1.4.0
gosrc.io/xmpp v0.1.1 gosrc.io/xmpp v0.1.1
) )

View File

@@ -2,7 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
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/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=
@@ -13,12 +12,6 @@ github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7/go.mod h1:E4vIYZDcEPVbE/D
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 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/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 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-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-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.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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/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/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@@ -27,26 +20,17 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 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/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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/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/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/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
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-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 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/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/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -54,31 +38,23 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 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.1/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/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=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 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/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/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=
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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -87,29 +63,15 @@ 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/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/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=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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/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/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.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/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.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -123,9 +85,7 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 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/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
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/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=
@@ -136,97 +96,65 @@ github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 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.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/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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/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= 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.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 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= 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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
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-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-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/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-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-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-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-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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-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-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-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-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-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-20181107165924-66b7b1311ac8/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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
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-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-20190813064441-fde4db37ae7a/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=
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-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-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/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/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/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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/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/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=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=

1
codecov.yml Normal file
View File

@@ -0,0 +1 @@
comment: off

5
codeship-services.yml Normal file
View File

@@ -0,0 +1,5 @@
build:
build:
image: fluux/build
dockerfile: Dockerfile
encrypted_env_file: codeship.env.encrypted

5
codeship-steps.yml Normal file
View File

@@ -0,0 +1,5 @@
- type: serial
steps:
- name: test
service: build
command: ./test.sh

1
codeship.env.encrypted Normal file
View File

@@ -0,0 +1 @@
yVKgVFeKW6SSnC/KgLYpfYtTcqqTke1gOIW5GUiVvRijnhweOJiYKFPmwPjpt1FVrg4WVELQUNbxn3lmfyHVVF7r

View File

@@ -1,19 +1,21 @@
package xmpp package xmpp
import ( import (
"context"
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
"gosrc.io/xmpp/stanza"
"io" "io"
"net"
"time"
"gosrc.io/xmpp/stanza"
) )
type ComponentOptions struct { const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
TransportConfiguration
type ComponentOptions struct {
// ================================= // =================================
// Component Connection Info // Component Connection Info
@@ -21,6 +23,9 @@ type ComponentOptions struct {
Domain string Domain string
// Secret is the "password" used by the XMPP server to secure component access // Secret is the "password" used by the XMPP server to secure component access
Secret string Secret string
// Address is the XMPP Host and port to connect to. Host is of
// the form 'serverhost:port' i.e "localhost:8888"
Address string
// ================================= // =================================
// Component discovery // Component discovery
@@ -45,15 +50,16 @@ type Component struct {
ComponentOptions ComponentOptions
router *Router router *Router
transport Transport // TCP level connection
conn net.Conn
// read / write // read / write
socketProxy io.ReadWriter // TODO socketProxy io.ReadWriter // TODO
ErrorHandler func(error) decoder *xml.Decoder
} }
func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*Component, error) { func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
c := Component{ComponentOptions: opts, router: r, ErrorHandler: errorHandler} c := Component{ComponentOptions: opts, router: r}
return &c, nil return &c, nil
} }
@@ -63,38 +69,39 @@ func (c *Component) Connect() error {
var state SMState var state SMState
return c.Resume(state) return c.Resume(state)
} }
func (c *Component) Resume(sm SMState) error { func (c *Component) Resume(sm SMState) error {
var conn net.Conn
var err error var err error
var streamId string if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil {
if c.ComponentOptions.TransportConfiguration.Domain == "" { return err
c.ComponentOptions.TransportConfiguration.Domain = c.ComponentOptions.Domain
}
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
if err != nil {
c.updateState(StatePermanentError)
return NewConnError(err, true)
}
if streamId, err = c.transport.Connect(); err != nil {
c.updateState(StatePermanentError)
return NewConnError(err, true)
} }
c.conn = conn
c.updateState(StateConnected) c.updateState(StateConnected)
// Authentication // 1. Send stream open tag
if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("<handshake>%s</handshake>", c.handshake(streamId)))); err != nil { if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
c.updateState(StateStreamError) c.updateState(StateStreamError)
return NewConnError(errors.New("cannot send stream open "+err.Error()), false)
}
c.decoder = xml.NewDecoder(conn)
// 2. Initialize xml decoder and extract streamID from reply
streamId, err := stanza.InitStream(c.decoder)
if err != nil {
c.updateState(StateStreamError)
return NewConnError(errors.New("cannot init decoder "+err.Error()), false)
}
// 3. Authentication
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
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 // 4. Check server response for authentication
val, err := stanza.NextPacket(c.transport.GetDecoder()) val, err := stanza.NextPacket(c.decoder)
if err != nil { if err != nil {
c.updateState(StatePermanentError) c.updateState(StateDisconnected)
return NewConnError(err, true) return NewConnError(err, true)
} }
@@ -106,20 +113,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 err // Should be empty at this point return nil
default: default:
c.updateState(StatePermanentError) c.updateState(StateStreamError)
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() error { func (c *Component) Disconnect() {
_ = c.SendRaw("</stream:stream>")
// 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 { conn := c.conn
return c.transport.Close() if conn != nil {
_ = conn.Close()
} }
// No transport so no connection.
return nil
} }
func (c *Component) SetHandler(handler EventHandler) { func (c *Component) SetHandler(handler EventHandler) {
@@ -127,26 +134,20 @@ func (c *Component) SetHandler(handler EventHandler) {
} }
// Receiver Go routine receiver // Receiver Go routine receiver
func (c *Component) recv() { func (c *Component) recv() (err error) {
for { for {
val, err := stanza.NextPacket(c.transport.GetDecoder()) val, err := stanza.NextPacket(c.decoder)
if err != nil { if err != nil {
c.updateState(StateDisconnected) c.updateState(StateDisconnected)
c.ErrorHandler(err) return 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)
c.ErrorHandler(errors.New("stream error: " + p.Error.Local)) return 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)
} }
@@ -154,8 +155,8 @@ func (c *Component) recv() {
// Send marshalls XMPP stanza and sends it to the server. // Send marshalls XMPP stanza and sends it to the server.
func (c *Component) Send(packet stanza.Packet) error { func (c *Component) Send(packet stanza.Packet) error {
transport := c.transport conn := c.conn
if transport == nil { if conn == nil {
return errors.New("component is not connected") return errors.New("component is not connected")
} }
@@ -164,49 +165,24 @@ 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 := c.sendWithWriter(transport, data); err != nil { if _, err := fmt.Fprintf(conn, string(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
// the provided handler function will automatically be called.
//
// The provided context should have a timeout to prevent the client from waiting
// forever for an IQ result. For example:
//
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
// result := <- client.SendIQ(ctx, iq)
//
func (c *Component) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
return nil, ErrCanOnlySendGetOrSetIq
}
if err := c.Send(iq); err != nil {
return nil, err
}
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
}
// SendRaw sends an XMPP stanza as a string to the server. // SendRaw sends an XMPP stanza as a string to the server.
// It can be invalid XML or XMPP content. In that case, the server will // It can be invalid XML or XMPP content. In that case, the server will
// disconnect the component. It is up to the user of this method to // disconnect the component. It is up to the user of this method to
// carefully craft the XML content to produce valid XMPP. // carefully craft the XML content to produce valid XMPP.
func (c *Component) SendRaw(packet string) error { func (c *Component) SendRaw(packet string) error {
transport := c.transport conn := c.conn
if transport == nil { if conn == nil {
return errors.New("component is not connected") return errors.New("component is not connected")
} }
var err error var err error
err = c.sendWithWriter(transport, []byte(packet)) _, err = fmt.Fprintf(c.conn, packet)
return err return err
} }

View File

@@ -1,22 +1,7 @@
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) {
@@ -35,71 +20,8 @@ func TestHandshake(t *testing.T) {
} }
} }
// Tests connection process with a handshake exchange func TestGenerateHandshake(t *testing.T) {
// Tests multiple session IDs. All serverConnections should generate a unique stream ID // TODO
func TestGenerateHandshakeId(t *testing.T) {
// 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)
fmt.Fprintln(sc.connection, "<handshake/>") // That's all the server needs to return (see xep-0114)
return
}
// 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.
m := make(map[string]bool)
for _, _ = range uuidsArray {
streamId, _ := c.transport.Connect()
m[c.handshake(streamId)] = true
}
if len(uuidsArray) != len(m) {
t.Errorf("Handshake does not produce a unique id. Expected: %d unique ids, got: %d", len(uuidsArray), len(m))
}
} }
// Test that NewStreamManager can accept a Component. // Test that NewStreamManager can accept a Component.
@@ -108,361 +30,3 @@ func TestGenerateHandshakeId(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) {
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, testSendIqPort, h)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
disco := iqReq.DiscoInfo()
iqReq.Payload = disco
// Handle a possible error
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 <-res:
case err := <-errChan:
t.Errorf(err.Error())
case <-time.After(defaultChannelTimeout):
t.Errorf("Failed to receive response, to sent IQ, from mock server")
}
select {
case <-done:
m.Stop()
case <-time.After(defaultChannelTimeout):
t.Errorf("The mock server failed to finish its job !")
}
cancel()
}
// 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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
// 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) {
sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
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)
fmt.Fprintln(sc.connection, string(s))
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)
fmt.Fprintln(sc.connection, "<handshake/>") // That's all the server needs to return (see xep-0114)
return
}

View File

@@ -1,24 +1,24 @@
package xmpp package xmpp
import ( import (
"gosrc.io/xmpp/stanza" "crypto/tls"
"io"
"os" "os"
"time"
) )
// Config & TransportConfiguration must not be modified after having been passed to NewClient. Any
// changes made after connecting are ignored.
type Config struct { type Config struct {
TransportConfiguration Address string
Jid string
Jid string parsedJid *Jid // For easier manipulation
parsedJid *stanza.Jid // For easier manipulation Password string
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 // tls.Config must not be modified after having been passed to NewClient. The
ConnectTimeout int // Client timeout in seconds. Default to 15 // Client connect method may override the tls.Config.ServerName if it was not set.
TLSConfig *tls.Config
// Insecure can be set to true to allow to open a session without TLS. If TLS // 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
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
} }

2
doc.go
View File

@@ -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
serverConnections. connections.
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).

9
go.mod
View File

@@ -1,13 +1,8 @@
module gosrc.io/xmpp module gosrc.io/xmpp
go 1.13 go 1.12
require ( require (
github.com/awesome-gocui/gocui v0.6.0 // indirect github.com/google/go-cmp v0.3.0
github.com/google/go-cmp v0.3.1
github.com/google/uuid v1.1.1
github.com/spf13/viper v1.6.1 // indirect
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
nhooyr.io/websocket v1.6.5
) )

204
go.sum
View File

@@ -1,210 +1,6 @@
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/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-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-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.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.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/fatih/color v1.6.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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
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/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
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.3.1/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.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=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
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/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/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/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/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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/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.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
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.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/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.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
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.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-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/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/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-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-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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/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/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-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-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-20190422165155-953cdadca894/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-20190813064441-fde4db37ae7a/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/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/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/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=

View File

@@ -1,4 +1,4 @@
package stanza package xmpp
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

View File

@@ -1,4 +1,4 @@
package stanza package xmpp
import ( import (
"testing" "testing"

View File

@@ -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 IPV6 with port // This is IPV$ 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

View File

@@ -1,10 +1,12 @@
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
@@ -31,36 +33,3 @@ 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)
}
})
}
}

View File

@@ -1,10 +1,8 @@
package xmpp package xmpp
import ( import (
"context"
"encoding/xml" "encoding/xml"
"strings" "strings"
"sync"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
@@ -27,35 +25,16 @@ TODO: Automatically reply to IQ that do not match any route, to comply to XMPP s
type Router struct { type Router struct {
// Routes to be matched, in order. // Routes to be matched, in order.
routes []*Route routes []*Route
IQResultRoutes map[string]*IQResultRoute
IQResultRouteLock sync.RWMutex
} }
// NewRouter returns a new router instance. // NewRouter returns a new router instance.
func NewRouter() *Router { func NewRouter() *Router {
return &Router{ return &Router{}
IQResultRoutes: make(map[string]*IQResultRoute),
}
} }
// 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)
if isIq {
r.IQResultRouteLock.RLock()
route, ok := r.IQResultRoutes[iq.Id]
r.IQResultRouteLock.RUnlock()
if ok {
r.IQResultRouteLock.Lock()
delete(r.IQResultRoutes, iq.Id)
r.IQResultRouteLock.Unlock()
route.result <- iq
close(route.result)
return
}
}
var match RouteMatch var match RouteMatch
if r.Match(p, &match) { if r.Match(p, &match) {
@@ -63,10 +42,11 @@ func (r *Router) route(s Sender, p stanza.Packet) {
match.Handler.HandlePacket(s, p) match.Handler.HandlePacket(s, p)
return return
} }
// If there is no match and we receive an iq set or get, we need to send a reply // If there is no match and we receive an iq set or get, we need to send a reply
if isIq && (iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet) { if iq, ok := p.(stanza.IQ); ok {
iqNotImplemented(s, iq) if iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet {
iqNotImplemented(s, iq)
}
} }
} }
@@ -88,27 +68,6 @@ func (r *Router) NewRoute() *Route {
return route return route
} }
// NewIQResultRoute register a route that will catch an IQ result stanza with
// the given Id. The route will only match ones, after which it will automatically
// be unregistered
func (r *Router) NewIQResultRoute(ctx context.Context, id string) chan stanza.IQ {
route := NewIQResultRoute(ctx)
r.IQResultRouteLock.Lock()
r.IQResultRoutes[id] = route
r.IQResultRouteLock.Unlock()
// Start a go function to make sure the route is unregistered when the context
// is done.
go func() {
<-route.context.Done()
r.IQResultRouteLock.Lock()
delete(r.IQResultRoutes, id)
r.IQResultRouteLock.Unlock()
}()
return route.result
}
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool { func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
for _, route := range r.routes { for _, route := range r.routes {
if route.Match(p, match) { if route.Match(p, match) {
@@ -130,44 +89,8 @@ func (r *Router) HandleFunc(name string, f func(s Sender, p stanza.Packet)) *Rou
return r.NewRoute().Packet(name).HandlerFunc(f) return r.NewRoute().Packet(name).HandlerFunc(f)
} }
// ============================================================================
// TimeoutHandlerFunc is a function type for handling IQ result timeouts.
type TimeoutHandlerFunc func(err error)
// IQResultRoute is a temporary route to match IQ result stanzas
type IQResultRoute struct {
context context.Context
result chan stanza.IQ
}
// NewIQResultRoute creates a new IQResultRoute instance
func NewIQResultRoute(ctx context.Context) *IQResultRoute {
return &IQResultRoute{
context: ctx,
result: make(chan stanza.IQ),
}
}
// ============================================================================
// IQ result handler
// IQResultHandler is a utility interface for IQ result handlers
type IQResultHandler interface {
HandleIQ(ctx context.Context, s Sender, iq stanza.IQ)
}
// IQResultHandlerFunc is an adapter to allow using functions as IQ result handlers.
type IQResultHandlerFunc func(ctx context.Context, s Sender, iq stanza.IQ)
// HandleIQ is a proxy function to implement IQResultHandler using a function.
func (f IQResultHandlerFunc) HandleIQ(ctx context.Context, s Sender, iq stanza.IQ) {
f(ctx, s, iq)
}
// ============================================================================ // ============================================================================
// Route // Route
type Handler interface { type Handler interface {
HandlePacket(s Sender, p stanza.Packet) HandlePacket(s Sender, p stanza.Packet)
} }

View File

@@ -2,10 +2,8 @@ package xmpp
import ( import (
"bytes" "bytes"
"context"
"encoding/xml" "encoding/xml"
"testing" "testing"
"time"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
@@ -13,47 +11,6 @@ import (
// ============================================================================ // ============================================================================
// Test route & matchers // Test route & matchers
func TestIQResultRoutes(t *testing.T) {
t.Parallel()
router := NewRouter()
conn := NewSenderMock()
if router.IQResultRoutes == nil {
t.Fatal("NewRouter does not initialize isResultRoutes")
}
// Check if the IQ handler was called
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel()
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, Id: "1234"})
res := router.NewIQResultRoute(ctx, "1234")
go router.route(conn, iq)
select {
case <-ctx.Done():
t.Fatal("IQ result was not matched")
case <-res:
// Success
}
// The match must only happen once, so the id should no longer be in IQResultRoutes
if _, ok := router.IQResultRoutes[iq.Attrs.Id]; ok {
t.Fatal("IQ ID was not removed from the route map")
}
// Check other IQ does not matcah
ctx, cancel = context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel()
iq.Attrs.Id = "4321"
res = router.NewIQResultRoute(ctx, "1234")
go router.route(conn, iq)
select {
case <-ctx.Done():
// Success
case <-res:
t.Fatal("IQ result with wrong ID was matched")
}
}
func TestNameMatcher(t *testing.T) { func TestNameMatcher(t *testing.T) {
router := NewRouter() router := NewRouter()
router.HandleFunc("message", func(s Sender, p stanza.Packet) { router.HandleFunc("message", func(s Sender, p stanza.Packet) {
@@ -146,7 +103,7 @@ 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: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &stanza.DiscoInfo{ iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{ XMLName: xml.Name{
Space: "jabber:iq:version", Space: "jabber:iq:version",
@@ -163,27 +120,27 @@ func TestCompositeMatcher(t *testing.T) {
router := NewRouter() router := NewRouter()
router.NewRoute(). router.NewRoute().
IQNamespaces("jabber:iq:version"). IQNamespaces("jabber:iq:version").
StanzaType(string(stanza.IQTypeGet)). StanzaType("get").
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: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
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: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"}) setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
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: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
GetDiscoIq.Payload = &stanza.DiscoInfo{ GetDiscoIq.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{ XMLName: xml.Name{
Space: "http://jabber.org/protocol/disco#info", Space: "http://jabber.org/protocol/disco#info",
@@ -238,7 +195,7 @@ func TestCatchallMatcher(t *testing.T) {
} }
conn = NewSenderMock() conn = NewSenderMock()
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &stanza.DiscoInfo{ iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{ XMLName: xml.Name{
Space: "jabber:iq:version", Space: "jabber:iq:version",
@@ -254,8 +211,7 @@ func TestCatchallMatcher(t *testing.T) {
// ============================================================================ // ============================================================================
// SenderMock // SenderMock
const successFlag = "matched" var successFlag = "matched"
const cancelledFlag = "cancelled"
type SenderMock struct { type SenderMock struct {
buffer *bytes.Buffer buffer *bytes.Buffer
@@ -274,15 +230,6 @@ 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) {
out, err := xml.Marshal(iq)
if err != nil {
return nil, err
}
s.buffer.Write(out)
return nil, nil
}
func (s SenderMock) SendRaw(str string) error { func (s SenderMock) SendRaw(str string) error {
s.buffer.WriteString(str) s.buffer.WriteString(str)
return nil return nil

View File

@@ -1,12 +1,18 @@
package xmpp package xmpp
import ( import (
"crypto/tls"
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"io"
"net"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
type Session struct { type Session struct {
// Session info // Session info
BindJid string // Jabber ID as provided by XMPP server BindJid string // Jabber ID as provided by XMPP server
@@ -17,42 +23,42 @@ type Session struct {
lastPacketId int lastPacketId int
// read / write // read / write
transport Transport streamLogger io.ReadWriter
decoder *xml.Decoder
// error management // error management
err error err error
} }
func NewSession(transport Transport, o Config, state SMState) (*Session, error) { func NewSession(conn net.Conn, o Config, state SMState) (net.Conn, *Session, error) {
s := new(Session) s := new(Session)
s.transport = transport
s.SMState = state s.SMState = state
s.init(o) s.init(conn, o)
// starttls
var tlsConn net.Conn
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o)
if s.err != nil { if s.err != nil {
return nil, NewConnError(s.err, true) return nil, nil, NewConnError(s.err, true)
} }
if !transport.IsSecure() { if !s.TlsEnabled && !o.Insecure {
s.startTlsIfSupported(o)
}
if !transport.IsSecure() && !o.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, nil, NewConnError(err, true)
} }
if s.TlsEnabled { if s.TlsEnabled {
s.reset(o) s.reset(conn, tlsConn, o)
} }
// auth // auth
s.auth(o) s.auth(o)
s.reset(o) s.reset(tlsConn, tlsConn, o)
// attempt resumption // attempt resumption
if s.resume(o) { if s.resume(o) {
return s, s.err return tlsConn, s, s.err
} }
// otherwise, bind resource and 'start' XMPP session // otherwise, bind resource and 'start' XMPP session
@@ -62,7 +68,7 @@ func NewSession(transport Transport, o Config, state SMState) (*Session, error)
// Enable stream management if supported // Enable stream management if supported
s.EnableStreamManagement(o) s.EnableStreamManagement(o)
return s, s.err return tlsConn, s, s.err
} }
func (s *Session) PacketId() string { func (s *Session) PacketId() string {
@@ -70,59 +76,91 @@ func (s *Session) PacketId() string {
return fmt.Sprintf("%x", s.lastPacketId) return fmt.Sprintf("%x", s.lastPacketId)
} }
func (s *Session) init(o Config) { func (s *Session) init(conn net.Conn, o Config) {
s.setStreamLogger(nil, conn, o)
s.Features = s.open(o.parsedJid.Domain) s.Features = s.open(o.parsedJid.Domain)
} }
func (s *Session) reset(o Config) { func (s *Session) reset(conn net.Conn, newConn net.Conn, o Config) {
if s.StreamId, s.err = s.transport.StartStream(); s.err != nil { if s.err != nil {
return return
} }
s.setStreamLogger(conn, newConn, o)
s.Features = s.open(o.parsedJid.Domain) s.Features = s.open(o.parsedJid.Domain)
} }
func (s *Session) setStreamLogger(conn net.Conn, newConn net.Conn, o Config) {
if newConn != conn {
s.streamLogger = newStreamLogger(newConn, o.StreamLogger)
}
s.decoder = xml.NewDecoder(s.streamLogger)
s.decoder.CharsetReader = o.CharsetReader
}
func (s *Session) open(domain string) (f stanza.StreamFeatures) { func (s *Session) open(domain string) (f stanza.StreamFeatures) {
// Send stream open tag
if _, s.err = fmt.Fprintf(s.streamLogger, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil {
return
}
// Set xml decoder and extract streamID from reply
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
if s.err != nil {
return
}
// extract stream features // extract stream features
if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil { if s.err = s.decoder.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())
} }
return return
} }
func (s *Session) startTlsIfSupported(o Config) { func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) net.Conn {
if s.err != nil { if s.err != nil {
return return conn
}
if !s.transport.DoesStartTLS() {
if !o.Insecure {
s.err = errors.New("Transport does not support starttls")
}
return
} }
if _, ok := s.Features.DoesStartTLS(); ok { if _, ok := s.Features.DoesStartTLS(); ok {
fmt.Fprintf(s.transport, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>") fmt.Fprintf(s.streamLogger, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
var k stanza.TLSProceed var k stanza.TLSProceed
if s.err = s.transport.GetDecoder().DecodeElement(&k, nil); s.err != nil { if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
s.err = errors.New("expecting starttls proceed: " + s.err.Error()) s.err = errors.New("expecting starttls proceed: " + s.err.Error())
return return conn
} }
s.err = s.transport.StartTLS() if o.TLSConfig == nil {
o.TLSConfig = &tls.Config{}
}
if o.TLSConfig.ServerName == "" {
o.TLSConfig.ServerName = domain
}
tlsConn := tls.Client(conn, o.TLSConfig)
// We convert existing connection to TLS
if s.err = tlsConn.Handshake(); s.err != nil {
return tlsConn
}
if !o.TLSConfig.InsecureSkipVerify {
s.err = tlsConn.VerifyHostname(domain)
}
if s.err == nil { if s.err == nil {
s.TlsEnabled = true s.TlsEnabled = true
} }
return return tlsConn
} }
// If we do not allow cleartext serverConnections, make it explicit that server do not support starttls // If we do not allow cleartext connections, 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")
} }
// starttls is not supported => we do not upgrade the connection:
return conn
} }
func (s *Session) auth(o Config) { func (s *Session) auth(o Config) {
@@ -130,7 +168,7 @@ func (s *Session) auth(o Config) {
return return
} }
s.err = authSASL(s.transport, s.transport.GetDecoder(), s.Features, o.parsedJid.Node, o.Credential) s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
} }
// Attempt to resume session using stream management // Attempt to resume session using stream management
@@ -142,11 +180,11 @@ func (s *Session) resume(o Config) bool {
return false return false
} }
fmt.Fprintf(s.transport, "<resume xmlns='%s' h='%d' previd='%s'/>", fmt.Fprintf(s.streamLogger, "<resume xmlns='%s' h='%d' previd='%s'/>",
stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id) stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id)
var packet stanza.Packet var packet stanza.Packet
packet, s.err = stanza.NextPacket(s.transport.GetDecoder()) packet, s.err = stanza.NextPacket(s.decoder)
if s.err == nil { if s.err == nil {
switch p := packet.(type) { switch p := packet.(type) {
case stanza.SMResumed: case stanza.SMResumed:
@@ -173,14 +211,14 @@ func (s *Session) bind(o Config) {
// 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 != "" { if resource != "" {
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>", fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
s.PacketId(), stanza.NSBind, resource) s.PacketId(), stanza.NSBind, resource)
} else { } else {
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind) fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
} }
var iq stanza.IQ var iq stanza.IQ
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil { if s.err = s.decoder.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())
return return
} }
@@ -205,8 +243,8 @@ 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) fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil { if s.err = s.decoder.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
} }
@@ -222,10 +260,10 @@ func (s *Session) EnableStreamManagement(o Config) {
return return
} }
fmt.Fprintf(s.transport, "<enable xmlns='%s' resume='true'/>", stanza.NSStreamManagement) fmt.Fprintf(s.streamLogger, "<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.decoder)
if s.err == nil { if s.err == nil {
switch p := packet.(type) { switch p := packet.(type) {
case stanza.SMEnabled: case stanza.SMEnabled:

View File

@@ -1,136 +0,0 @@
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 `xml:",any"`
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"`
}
func (c *Command) Namespace() string {
return c.XMLName.Space
}
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 (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{}
d.DecodeElement(&a, &tt)
c.CommandElement = &a
case "configure":
nt := Note{}
d.DecodeElement(&nt, &tt)
c.CommandElement = &nt
default:
n := Node{}
e := d.DecodeElement(&n, &tt)
_ = e
c.CommandElement = &n
if err != nil {
return err
}
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}

View File

@@ -1,53 +0,0 @@
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 xmlns=\"jabber:x:data\">Available Servi" +
"ces</title><reported xmlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" label=\"S" +
"ervice\" var=\"service\"></field><field xmlns=\"jabber:x:data\" label=\"Single-User mo" +
"de\" var=\"runlevel-1\"></field><field xmlns=\"jabber:x:data\" label=\"Non-Networked M" +
"ulti-User mode\" var=\"runlevel-2\"></field><field xmlns=\"jabber:x:data\" label=\"Ful" +
"l Multi-User mode\" var=\"runlevel-3\"></field><field xmlns=\"jabber:x:data\" label=\"" +
"X-Window mode\" var=\"runlevel-5\"></field></reported><item xmlns=\"jabber:x:data\"><" +
"field xmlns=\"jabber:x:data\" var=\"service\"><value xmlns=\"jabber:x:data\">httpd</va" +
"lue></field><field xmlns=\"jabber:x:data\" var=\"runlevel-1\"><value xmlns=\"jabber:x" +
":data\">off</value></field><field xmlns=\"jabber:x:data\" var=\"runlevel-2\"><value x" +
"mlns=\"jabber:x:data\">off</value></field><field xmlns=\"jabber:x:data\" var=\"runlev" +
"el-3\"><value xmlns=\"jabber:x:data\">on</value></field><field xmlns=\"jabber:x:data" +
"\" var=\"runlevel-5\"><value xmlns=\"jabber:x:data\">on</value></field></item><item x" +
"mlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" var=\"service\"><value xmlns=\"ja" +
"bber:x:data\">postgresql</value></field><field xmlns=\"jabber:x:data\" var=\"runleve" +
"l-1\"><value xmlns=\"jabber:x:data\">off</value></field><field xmlns=\"jabber:x:data" +
"\" var=\"runlevel-2\"><value xmlns=\"jabber:x:data\">off</value></field><field xmlns=" +
"\"jabber:x:data\" var=\"runlevel-3\"><value xmlns=\"jabber:x:data\">on</value></field>" +
"<field xmlns=\"jabber:x:data\" var=\"runlevel-5\"><value xmlns=\"jabber:x:data\">on</v" +
"alue></field></item><item xmlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" var" +
"=\"service\"><value xmlns=\"jabber:x:data\">jabberd</value></field><field xmlns=\"jab" +
"ber:x:data\" var=\"runlevel-1\"><value xmlns=\"jabber:x:data\">off</value></field><fi" +
"eld xmlns=\"jabber:x:data\" var=\"runlevel-2\"><value xmlns=\"jabber:x:data\">off</val" +
"ue></field><field xmlns=\"jabber:x:data\" var=\"runlevel-3\"><value xmlns=\"jabber:x:" +
"data\">on</value></field><field xmlns=\"jabber:x:data\" var=\"runlevel-5\"><value xml" +
"ns=\"jabber:x:data\">on</value></field></item></x></command>"
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())
}
}

View File

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

View File

@@ -67,7 +67,7 @@ func TestParsingDelegationIQ(t *testing.T) {
return return
} }
if forwardedIQ.Payload != nil { if forwardedIQ.Payload != nil {
if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); ok { if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
node = pubsub.Publish.Node node = pubsub.Publish.Node
} }
} }

View File

@@ -3,7 +3,6 @@ package stanza
import ( import (
"encoding/xml" "encoding/xml"
"strconv" "strconv"
"strings"
) )
// ============================================================================ // ============================================================================
@@ -54,19 +53,10 @@ 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"}
// TODO : change the pubsub handling ? It kind of dilutes the information if elt.XMLName == textName {
// Handles : 6.1.3.11 Node Has Moved for XEP-0060 (PubSubGeneric) x.Text = string(elt.Content)
goneName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "gone"} } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
if elt.XMLName == textName || // Regular error text x.Reason = elt.XMLName.Local
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:
@@ -104,32 +94,16 @@ 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}
err = e.EncodeToken(xml.StartElement{Name: reason}) e.EncodeToken(xml.StartElement{Name: reason})
if err != nil { e.EncodeToken(xml.EndElement{Name: reason})
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"}
err = e.EncodeToken(xml.StartElement{Name: text}) e.EncodeToken(xml.StartElement{Name: text})
if err != nil { e.EncodeToken(xml.CharData(x.Text))
return err e.EncodeToken(xml.EndElement{Name: text})
}
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})

View File

@@ -1,67 +0,0 @@
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
Type string `xml:"type,attr"`
}
type FormItem struct {
Fields []Field
}
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"`
}

View File

@@ -1,107 +0,0 @@
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 := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet})
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)
}
}

View File

@@ -2,9 +2,6 @@ package stanza
import ( import (
"encoding/xml" "encoding/xml"
"strings"
"github.com/google/uuid"
) )
/* /*
@@ -24,7 +21,7 @@ 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"`
} }
@@ -34,12 +31,8 @@ type IQPayload interface {
} }
func NewIQ(a Attrs) IQ { func NewIQ(a Attrs) IQ {
// TODO generate IQ ID if not set
// TODO ensure that type is set, as it is required // TODO ensure that type is set, as it is required
if a.Id == "" {
if id, err := uuid.NewRandom(); err == nil {
a.Id = id.String()
}
}
return IQ{ return IQ{
XMLName: xml.Name{Local: "iq"}, XMLName: xml.Name{Local: "iq"},
Attrs: a, Attrs: a,
@@ -53,7 +46,7 @@ func (iq IQ) MakeError(xerror Err) IQ {
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
} }
@@ -107,7 +100,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 {
@@ -133,39 +126,3 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
} }
} }
} }
// Following RFC-3920 for IQs
func (iq *IQ) IsValid() bool {
// ID is required
if len(strings.TrimSpace(iq.Id)) == 0 {
return false
}
// Type is required
if iq.Type.IsEmpty() {
return false
}
// 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
}
}
// A result must include zero or one child element
if iq.Type == IQTypeResult {
if iq.Payload != nil && iq.Any != nil {
return false
}
}
//Error type must contain an "error" child element
if iq.Type == IQTypeError {
if iq.Error == nil {
return false
}
}
return true
}

View File

@@ -8,7 +8,6 @@ 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"
) )
@@ -22,7 +21,6 @@ type DiscoInfo struct {
Features []Feature `xml:"feature"` Features []Feature `xml:"feature"`
} }
// 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
} }
@@ -114,7 +112,7 @@ func (d *DiscoItems) Namespace() string {
// 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: NSDiscoItems, Local: "query"}, XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", Local: "query"},
} }
iq.Payload = &d iq.Payload = &d
return &d return &d

View File

@@ -50,7 +50,7 @@ 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: stanza.IQTypeResult, From: "catalog.shakespeare.lit", iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "catalog.shakespeare.lit",
To: "romeo@montague.net/orchard", Id: "items-2"}) To: "romeo@montague.net/orchard", Id: "items-2"})
iq.DiscoItems(). iq.DiscoItems().
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare"). AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
@@ -73,11 +73,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("List length mismatch: %#v", pp.Items) t.Errorf("Items 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)

View File

@@ -1,115 +0,0 @@
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"`
}
// Namespace defines the namespace for the RosterIQ
func (r *Roster) Namespace() string {
return r.XMLName.Space
}
// ---------------
// 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"`
}
// Namespace lets RosterItems implement the IQPayload interface
func (r *RosterItems) Namespace() string {
return r.XMLName.Space
}
// 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{})
}

View File

@@ -1,109 +0,0 @@
package stanza
import (
"encoding/xml"
"reflect"
"testing"
)
func TestRosterBuilder(t *testing.T) {
iq := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"})
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
}

View File

@@ -34,24 +34,6 @@ func TestUnmarshalIqs(t *testing.T) {
} }
} }
func TestGenerateIqId(t *testing.T) {
t.Parallel()
iq := stanza.NewIQ(stanza.Attrs{Id: "1"})
if iq.Id != "1" {
t.Errorf("NewIQ replaced id with %s", iq.Id)
}
iq = stanza.NewIQ(stanza.Attrs{})
if iq.Id == "" {
t.Error("NewIQ did not generate an Id")
}
otherIq := stanza.NewIQ(stanza.Attrs{})
if iq.Id == otherIq.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 := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
payload := stanza.DiscoInfo{ payload := stanza.DiscoInfo{
@@ -187,38 +169,3 @@ 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
}
if !parsedIQ.IsValid() && !tcase.shouldErr {
t.Errorf("failed iq validation for : %s", tcase.iq)
}
})
}
}

View File

@@ -1,214 +0,0 @@
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
}
}
}
return nil
}

View File

@@ -1,162 +0,0 @@
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))
}
}

View File

@@ -46,18 +46,9 @@ 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)
if err != nil { e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
return err
}
err = e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
if err != nil {
return err
}
if n.Content != "" { if n.Content != "" {
err = e.EncodeToken(xml.CharData(n.Content)) 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})
} }

View File

@@ -6,7 +6,6 @@ const (
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl" NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
NSBind = "urn:ietf:params:xml:ns:xmpp-bind" NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
NSSession = "urn:ietf:params:xml:ns:xmpp-session" NSSession = "urn:ietf:params:xml:ns:xmpp-session"
NSFraming = "urn:ietf:params:xml:ns:xmpp-framing"
NSClient = "jabber:client" NSClient = "jabber:client"
NSComponent = "jabber:component:accept" NSComponent = "jabber:component:accept"
) )

View File

@@ -1,13 +0,0 @@
package stanza
import "encoding/xml"
// Open Packet
// Reference: WebSocket connections must start with this element
// https://tools.ietf.org/html/rfc7395#section-3.4
type WebsocketOpen struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-framing open"`
From string `xml:"from,attr"`
Id string `xml:"id,attr"`
Version string `xml:"version,attr"`
}

View File

@@ -1,7 +1,5 @@
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
@@ -25,7 +23,3 @@ 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
}

View File

@@ -24,10 +24,8 @@ func InitStream(p *xml.Decoder) (sessionID string, err error) {
switch elem := t.(type) { switch elem := t.(type) {
case xml.StartElement: case xml.StartElement:
isStreamOpen := elem.Name.Space == NSStream && elem.Name.Local == "stream" if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
isFrameOpen := elem.Name.Space == NSFraming && elem.Name.Local == "open" err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
if !isStreamOpen && !isFrameOpen {
err = errors.New("xmpp: expected <stream> or <open> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
return sessionID, err return sessionID, err
} }
@@ -50,20 +48,11 @@ 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
t, err := NextXmppToken(p) se, err := NextStart(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:
@@ -82,29 +71,7 @@ func NextPacket(p *xml.Decoder) (Packet, error) {
} }
} }
// NextXmppToken scans XML token stream to find next StartElement or stream EndElement. // Scan XML token stream to find next StartElement.
// 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()
@@ -128,29 +95,16 @@ 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, t xml.Token) (Packet, error) { func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) {
if se, ok := t.(xml.StartElement); ok { switch se.Name.Local {
switch se.Name.Local { case "error":
case "error": return streamError.decode(p, se)
return streamError.decode(p, se) case "features":
case "features": return streamFeatures.decode(p, se)
return streamFeatures.decode(p, se) default:
default:
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 " + return nil, errors.New("unexpected XMPP packet " +
ee.Name.Space + " <" + ee.Name.Local + "/>") se.Name.Space + " <" + se.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.

View File

@@ -15,7 +15,7 @@ type Tune struct {
Uri string `xml:"uri,omitempty"` Uri string `xml:"uri,omitempty"`
} }
// Mood defines data model for XEP-0107 - User Mood // Mood defines deta 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

View File

@@ -2,383 +2,39 @@ package stanza
import ( import (
"encoding/xml" "encoding/xml"
"errors"
"strings"
) )
type PubSubGeneric struct { type PubSub struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"` XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
Publish *Publish
Create *Create `xml:"create,omitempty"` Retract *Retract
Configure *Configure `xml:"configure,omitempty"` // TODO <configure/>
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"`
} }
func (p *PubSubGeneric) Namespace() string { func (p *PubSub) Namespace() string {
return p.XMLName.Space return p.XMLName.Space
} }
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"`
Items []Item `xml:"item,omitempty"` // xsd says there can be many. See also 12.10 Batch Processing of XEP-0060 Item Item
}
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"`
Publisher string `xml:"publisher,attr,omitempty"` Tune *Tune
Any *Node `xml:",any"` Mood *Mood
} }
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 *bool `xml:"notify,attr,omitempty"` Notify string `xml:"notify,attr"`
Items []Item `xml:"item"` Item 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 IQ{}, e
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 IQ{}, e
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 IQ{}, e
}
iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
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 IQ{}, e
}
if form.Type != FormTypeSubmit {
return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type)
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 IQ{}, e
}
if form.Type != FormTypeSubmit {
return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type)
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
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 IQ{}, errors.New("cannot publish without a target node ID")
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 IQ{}, errors.New("cannot publish without a target node ID")
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 IQ{}, errors.New("cannot delete item without a target node ID")
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
iq.Payload = &PubSubGeneric{
Affiliations: &Affiliations{},
}
return iq, nil
} }
func init() { func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub", Local: "pubsub"}, PubSubGeneric{}) TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{})
} }

View File

@@ -1,377 +0,0 @@
package stanza
import (
"encoding/xml"
"errors"
"strings"
)
type PubSubOwner struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"`
OwnerUseCase OwnerUseCase
}
func (pso *PubSubOwner) Namespace() string {
return pso.XMLName.Space
}
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
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 IQ{}, errors.New("cannot delete a node without a target node ID")
}
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 IQ{}, 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 IQ{}, err
}
var n Node
xml.Unmarshal(data, &n)
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
iq.Payload = &PubSubOwner{
OwnerUseCase: &SubscriptionsOwner{Node: nodeID},
}
return iq, nil
}
func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (IQ, error) {
iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
iq.Payload = &PubSubOwner{
OwnerUseCase: &AffiliationsOwner{
Node: nodeID,
},
}
return iq, 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
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{}
d.DecodeElement(&aff, &tt)
pso.OwnerUseCase = &aff
case "configure":
co := ConfigureOwner{}
d.DecodeElement(&co, &tt)
pso.OwnerUseCase = &co
case "default":
def := DefaultOwner{}
d.DecodeElement(&def, &tt)
pso.OwnerUseCase = &def
case "delete":
del := DeleteOwner{}
d.DecodeElement(&del, &tt)
pso.OwnerUseCase = &del
case "purge":
pu := PurgeOwner{}
d.DecodeElement(&pu, &tt)
pso.OwnerUseCase = &pu
case "subscriptions":
subs := SubscriptionsOwner{}
d.DecodeElement(&subs, &tt)
pso.OwnerUseCase = &subs
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{})
}

View File

@@ -1,833 +0,0 @@
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")
subR.Id = "config1"
if err != nil {
t.Fatalf("Could not create 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.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")
subR.Id = "def1"
if err != nil {
t.Fatalf("Could not create 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.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")
subR.Id = "delete1"
if err != nil {
t.Fatalf("Could not create 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.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")
subR.Id = "purge1"
if err != nil {
t.Fatalf("Could not create 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.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)
subR.Id = "approve1"
if err != nil {
t.Fatalf("Could not create request : %s", err)
}
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")
subR.Id = "pending1"
if err != nil {
t.Fatalf("Could not create request : %s", err)
}
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 {
errors.New("this iq payload is not a command")
}
fMap, err := respIQ.GetFormFields()
if err != nil || len(fMap) != 2 {
errors.New("could not parse command form fields")
}
}
// ********************************************
// * 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")
subR.Id = "pending2"
if err != nil {
t.Fatalf("Could not create request : %s", err)
}
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")
subR.Id = "subman1"
if err != nil {
t.Fatalf("Could not create 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.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 {
errors.New("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")
subR.Id = "ent1"
if err != nil {
t.Fatalf("Could not create 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.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 {
errors.New("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)
subR.Id = "ent3"
if err != nil {
t.Fatalf("Could not create 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.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 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 {
errors.New("this iq payload is not a pubsub of the owner namespace")
}
return pubsub, nil
}

View File

@@ -1,921 +0,0 @@
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)
subR.Id = "sub1"
if err != nil {
t.Fatalf("Could not create a sub request : %s", err)
}
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)
subR.Id = "unsub1"
if err != nil {
t.Fatalf("Could not create a sub 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.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)
subR.Id = "options1"
if err != nil {
t.Fatalf("Could not create a sub 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.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)
subR.Id = "options2"
if err != nil {
t.Fatalf("Could not create a sub 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.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)
subR.Id = "sub1"
if err != nil {
t.Fatalf("Could not create a sub 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.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")
subR.Id = "items3"
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")
}
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)
subR.Id = "retract1"
if err != nil {
t.Fatalf("Could not create a del item request : %s", err)
}
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")
subR.Id = "create1"
if err != nil {
t.Fatalf("Could not create a create node request : %s", err)
}
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"}},
},
})
subR.Id = "create1"
if err != nil {
t.Fatalf("Could not create a create node request : %s", err)
}
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")
subR.Id = "subscriptions1"
if err != nil {
t.Fatalf("Could not create a create node request : %s", err)
}
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")
subR.Id = "affil1"
if err != nil {
t.Fatalf("Could not create retreive all affiliations request : %s", err)
}
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 {
errors.New("this iq payload is not a pubsub")
}
return pubsub, nil
}

View File

@@ -107,6 +107,6 @@ func (s *StreamSession) IsOptional() bool {
// Registry init // Registry init
func init() { func init() {
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-bind", "bind"}, Bind{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-session", Local: "session"}, StreamSession{}) TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-session", "session"}, StreamSession{})
} }

View File

@@ -1,16 +1,167 @@
package stanza package stanza
import "encoding/xml" import (
"encoding/xml"
)
// Start of stream // ============================================================================
// Reference: XMPP Core stream open // StreamFeatures Packet
// https://tools.ietf.org/html/rfc6120#section-4.2 // Reference: The active stream features are published on
type Stream struct { // https://xmpp.org/registrar/stream-features.html
XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"` // Note: That page misses draft and experimental XEP (i.e CSI, etc)
From string `xml:"from,attr"`
To string `xml:"to,attr"` type StreamFeatures struct {
Id string `xml:"id,attr"` XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
Version string `xml:"version,attr"` // Server capabilities hash
Caps Caps
// Stream features
StartTLS tlsStartTLS
Mechanisms saslMechanisms
Bind Bind
StreamManagement streamManagement
// Obsolete
Session StreamSession
// ProcessOne Stream Features
P1Push p1Push
P1Rebind p1Rebind
p1Ack p1Ack
Any []xml.Name `xml:",any"`
} }
const StreamClose = "</stream:stream>" func (StreamFeatures) Name() string {
return "stream:features"
}
type streamFeatureDecoder struct{}
var streamFeatures streamFeatureDecoder
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
var packet StreamFeatures
err := p.DecodeElement(&packet, &se)
return packet, err
}
// Capabilities
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
// and peer servers do not need to send service discovery requests each time they connect."
// This is not a stream feature but a way to let client cache server disco info.
type Caps struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
Hash string `xml:"hash,attr"`
Node string `xml:"node,attr"`
Ver string `xml:"ver,attr"`
Ext string `xml:"ext,attr,omitempty"`
}
// ============================================================================
// Supported Stream Features
// StartTLS feature
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
type tlsStartTLS struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
Required bool
}
// UnmarshalXML implements custom parsing startTLS required flag
func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
stls.XMLName = start.Name
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
elt := new(Node)
err = d.DecodeElement(elt, &tt)
if err != nil {
return err
}
if elt.XMLName.Local == "required" {
stls.Required = true
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (sf *StreamFeatures) DoesStartTLS() (feature tlsStartTLS, isSupported bool) {
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
return sf.StartTLS, true
}
return feature, false
}
// Mechanisms
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
type saslMechanisms struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
Mechanism []string `xml:"mechanism"`
}
// StreamManagement
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
type streamManagement struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
}
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
return true
}
return false
}
// P1 extensions
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
// p1:push support
type p1Push struct {
XMLName xml.Name `xml:"p1:push push"`
}
// p1:rebind suppor
type p1Rebind struct {
XMLName xml.Name `xml:"p1:rebind rebind"`
}
// p1:ack support
type p1Ack struct {
XMLName xml.Name `xml:"p1:ack ack"`
}
// ============================================================================
// StreamError Packet
type StreamError struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
Error xml.Name `xml:",any"`
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
}
func (StreamError) Name() string {
return "stream:error"
}
type streamErrorDecoder struct{}
var streamError streamErrorDecoder
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
var packet StreamError
err := p.DecodeElement(&packet, &se)
return packet, err
}

View File

@@ -1,185 +0,0 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// StreamFeatures Packet
// Reference: The active stream features are published on
// https://xmpp.org/registrar/stream-features.html
// Note: That page misses draft and experimental XEP (i.e CSI, etc)
type StreamFeatures struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
// Server capabilities hash
Caps Caps
// Stream features
StartTLS TlsStartTLS
Mechanisms saslMechanisms
Bind Bind
StreamManagement streamManagement
// Obsolete
Session StreamSession
// ProcessOne Stream Features
P1Push p1Push
P1Rebind p1Rebind
p1Ack p1Ack
Any []xml.Name `xml:",any"`
}
func (StreamFeatures) Name() string {
return "stream:features"
}
type streamFeatureDecoder struct{}
var streamFeatures streamFeatureDecoder
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
var packet StreamFeatures
err := p.DecodeElement(&packet, &se)
return packet, err
}
// Capabilities
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
// and peer servers do not need to send service discovery requests each time they connect."
// This is not a stream feature but a way to let client cache server disco info.
type Caps struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
Hash string `xml:"hash,attr"`
Node string `xml:"node,attr"`
Ver string `xml:"ver,attr"`
Ext string `xml:"ext,attr,omitempty"`
}
// ============================================================================
// Supported Stream Features
// StartTLS feature
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
type TlsStartTLS struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
Required bool
}
// UnmarshalXML implements custom parsing startTLS required flag
func (stls *TlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
stls.XMLName = start.Name
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
elt := new(Node)
err = d.DecodeElement(elt, &tt)
if err != nil {
return err
}
if elt.XMLName.Local == "required" {
stls.Required = true
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (sf *StreamFeatures) DoesStartTLS() (feature TlsStartTLS, isSupported bool) {
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
return sf.StartTLS, true
}
return feature, false
}
// Mechanisms
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
type saslMechanisms struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
Mechanism []string `xml:"mechanism"`
}
// StreamManagement
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
type streamManagement struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
}
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
return true
}
return false
}
// P1 extensions
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
// p1:push support
type p1Push struct {
XMLName xml.Name `xml:"p1:push push"`
}
// p1:rebind suppor
type p1Rebind struct {
XMLName xml.Name `xml:"p1:rebind rebind"`
}
// p1:ack support
type p1Ack struct {
XMLName xml.Name `xml:"p1:ack ack"`
}
// ============================================================================
// StreamError Packet
type StreamError struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
Error xml.Name `xml:",any"`
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
}
func (StreamError) Name() string {
return "stream:error"
}
type streamErrorDecoder struct{}
var streamError streamErrorDecoder
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
var packet StreamError
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// 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{}
}

View File

@@ -2,17 +2,12 @@ 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
@@ -68,14 +63,3 @@ 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
}

View File

@@ -2,16 +2,17 @@ package xmpp
import ( import (
"io" "io"
"os"
) )
// Mediated Read / Write on socket // Mediated Read / Write on socket
// Used if logFile from Config is not nil // Used if logFile from Config is not nil
type streamLogger struct { type streamLogger struct {
socket io.ReadWriter // Actual connection socket io.ReadWriter // Actual connection
logFile io.Writer logFile *os.File
} }
func newStreamLogger(conn io.ReadWriter, logFile io.Writer) io.ReadWriter { func newStreamLogger(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
if logFile == nil { if logFile == nil {
return conn return conn
} else { } else {
@@ -19,21 +20,21 @@ func newStreamLogger(conn io.ReadWriter, logFile io.Writer) io.ReadWriter {
} }
} }
func (sl *streamLogger) Read(p []byte) (n int, err error) { func (sp *streamLogger) Read(p []byte) (n int, err error) {
n, err = sl.socket.Read(p) n, err = sp.socket.Read(p)
if n > 0 { if n > 0 {
sl.logFile.Write([]byte("RECV:\n")) // Prefix sp.logFile.Write([]byte("RECV:\n")) // Prefix
if n, err := sl.logFile.Write(p[:n]); err != nil { if n, err := sp.logFile.Write(p[:n]); err != nil {
return n, err return n, err
} }
sl.logFile.Write([]byte("\n\n")) // Separator sp.logFile.Write([]byte("\n\n")) // Separator
} }
return return
} }
func (sl *streamLogger) Write(p []byte) (n int, err error) { func (sp *streamLogger) Write(p []byte) (n int, err error) {
sl.logFile.Write([]byte("SEND:\n")) // Prefix sp.logFile.Write([]byte("SEND:\n")) // Prefix
for _, w := range []io.Writer{sl.socket, sl.logFile} { for _, w := range []io.Writer{sp.socket, sp.logFile} {
n, err = w.Write(p) n, err = w.Write(p)
if err != nil { if err != nil {
return return
@@ -43,7 +44,7 @@ func (sl *streamLogger) Write(p []byte) (n int, err error) {
return return
} }
} }
sl.logFile.Write([]byte("\n\n")) // Separator sp.logFile.Write([]byte("\n\n")) // Separator
return len(p), nil return len(p), nil
} }

View File

@@ -1,7 +1,6 @@
package xmpp package xmpp
import ( import (
"context"
"errors" "errors"
"sync" "sync"
"time" "time"
@@ -27,9 +26,8 @@ type StreamClient interface {
Connect() error Connect() error
Resume(state SMState) error Resume(state SMState) error
Send(packet stanza.Packet) error Send(packet stanza.Packet) error
SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error)
SendRaw(packet string) error SendRaw(packet string) error
Disconnect() error Disconnect()
SetHandler(handler EventHandler) SetHandler(handler EventHandler)
} }
@@ -37,7 +35,6 @@ 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)
SendRaw(packet string) error SendRaw(packet string) error
} }
@@ -74,7 +71,7 @@ func (sm *StreamManager) Run() error {
return errors.New("missing stream client") return errors.New("missing stream client")
} }
handler := func(e Event) error { handler := func(e Event) {
switch e.State { switch e.State {
case StateConnected: case StateConnected:
sm.Metrics.setConnectTime() sm.Metrics.setConnectTime()
@@ -82,18 +79,14 @@ func (sm *StreamManager) Run() error {
sm.Metrics.setLoginTime() sm.Metrics.setLoginTime()
case StateDisconnected: case StateDisconnected:
// Reconnect on disconnection // Reconnect on disconnection
return sm.resume(e.SMState) sm.resume(e.SMState)
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" {
return sm.connect() sm.connect()
} }
case StatePermanentError:
// Do not attempt to reconnect
} }
return nil
} }
sm.client.SetHandler(handler) sm.client.SetHandler(handler)

View File

@@ -1,61 +1,26 @@
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
)
// 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, serverConn *ServerConn) type ClientHandler func(t *testing.T, conn net.Conn)
// 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
serverConnections []*ServerConn connections []net.Conn
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.
@@ -73,9 +38,9 @@ func (mock *ServerMock) Stop() {
if mock.listener != nil { if mock.listener != nil {
mock.listener.Close() mock.listener.Close()
} }
// Close all existing serverConnections // Close all existing connections
for _, c := range mock.serverConnections { for _, c := range mock.connections {
c.connection.Close() c.Close()
} }
} }
@@ -95,14 +60,13 @@ func (mock *ServerMock) init(addr string) error {
return nil return nil
} }
// loop accepts serverConnections and creates a go routine per connection. // loop accepts connections 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:
@@ -112,195 +76,8 @@ func (mock *ServerMock) loop() {
} }
return return
} }
mock.serverConnections = append(mock.serverConnections, serverConn) mock.connections = append(mock.connections, conn)
// 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, serverConn) go mock.handler(mock.t, conn)
}
}
//======================================================================================================================
// 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 !iqReq.IsValid() {
mockIQError(sc.connection)
return
}
// Crafting response
iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"})
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) {
sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
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)
}
}
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) {
sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
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)
} }
} }

View File

@@ -5,9 +5,13 @@ export GO111MODULE=on
echo "" > coverage.txt echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do for d in $(go list ./... | grep -v vendor); do
go test -race -coverprofile=profile.out -covermode=atomic "${d}" go test -race -coverprofile=profile.out -covermode=atomic ${d}
if [ -f profile.out ]; then if [ -f profile.out ]; then
cat profile.out >> coverage.txt cat profile.out >> coverage.txt
rm profile.out rm profile.out
fi fi
done done
if [ -f "./codecov.sh" ]; then
./codecov.sh
fi

View File

@@ -1,78 +0,0 @@
package xmpp
import (
"crypto/tls"
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
)
var ErrTransportProtocolNotSupported = errors.New("Transport protocol not supported")
var ErrTLSNotSupported = errors.New("Transport does not support StartTLS")
// TODO: rename to transport config?
type TransportConfiguration struct {
// Address is the XMPP Host and port to connect to. Host is of
// the form 'serverhost:port' i.e "localhost:8888"
Address string
Domain string
ConnectTimeout int // Client timeout in seconds. Default to 15
// tls.Config must not be modified after having been passed to NewClient. Any
// changes made after connecting are ignored.
TLSConfig *tls.Config
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
}
type Transport interface {
Connect() (string, error)
DoesStartTLS() bool
StartTLS() error
LogTraffic(logFile io.Writer)
StartStream() (string, error)
GetDecoder() *xml.Decoder
IsSecure() bool
Ping() error
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
// ReceivedStreamClose signals to the transport that a </stream:stream> has been received and that the tcp connection
// should be closed.
ReceivedStreamClose()
}
// NewClientTransport creates a new Transport instance for clients.
// The type of transport is determined by the address in the configuration:
// - if the address is a URL with the `ws` or `wss` scheme WebsocketTransport is used
// - in all other cases a XMPPTransport is used
// For XMPPTransport it is mandatory for the address to have a port specified.
func NewClientTransport(config TransportConfiguration) Transport {
if strings.HasPrefix(config.Address, "ws:") || strings.HasPrefix(config.Address, "wss:") {
return &WebsocketTransport{Config: config}
}
config.Address = ensurePort(config.Address, 5222)
return &XMPPTransport{
Config: config,
openStatement: clientStreamOpen,
}
}
// NewComponentTransport creates a new Transport instance for components.
// Only XMPP transports are allowed. If you try to use any other protocol an error
// will be returned.
func NewComponentTransport(config TransportConfiguration) (Transport, error) {
if strings.HasPrefix(config.Address, "ws:") || strings.HasPrefix(config.Address, "wss:") {
return nil, fmt.Errorf("Components only support XMPP transport: %w", ErrTransportProtocolNotSupported)
}
config.Address = ensurePort(config.Address, 5222)
return &XMPPTransport{
Config: config,
openStatement: componentStreamOpen,
}, nil
}

View File

@@ -1,185 +0,0 @@
package xmpp
import (
"bufio"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
"time"
"gosrc.io/xmpp/stanza"
"nhooyr.io/websocket"
)
const maxPacketSize = 32768
const pingTimeout = time.Duration(5) * time.Second
var ServerDoesNotSupportXmppOverWebsocket = errors.New("the websocket server does not support the xmpp subprotocol")
// The decoder is expected to be initialized after connecting to a server.
type WebsocketTransport struct {
Config TransportConfiguration
decoder *xml.Decoder
wsConn *websocket.Conn
queue chan []byte
logFile io.Writer
closeCtx context.Context
closeFunc context.CancelFunc
}
func (t *WebsocketTransport) Connect() (string, error) {
t.queue = make(chan []byte, 256)
t.closeCtx, t.closeFunc = context.WithCancel(context.Background())
var ctx context.Context
ctx = context.Background()
if t.Config.ConnectTimeout > 0 {
var cancelConnect context.CancelFunc
ctx, cancelConnect = context.WithTimeout(t.closeCtx, time.Duration(t.Config.ConnectTimeout)*time.Second)
defer cancelConnect()
}
wsConn, response, err := websocket.Dial(ctx, t.Config.Address, &websocket.DialOptions{
Subprotocols: []string{"xmpp"},
})
if err != nil {
return "", NewConnError(err, true)
}
if response.Header.Get("Sec-WebSocket-Protocol") != "xmpp" {
t.cleanup(websocket.StatusBadGateway)
return "", NewConnError(ServerDoesNotSupportXmppOverWebsocket, true)
}
wsConn.SetReadLimit(maxPacketSize)
t.wsConn = wsConn
t.startReader()
t.decoder = xml.NewDecoder(bufio.NewReaderSize(t, maxPacketSize))
t.decoder.CharsetReader = t.Config.CharsetReader
return t.StartStream()
}
func (t WebsocketTransport) StartStream() (string, error) {
if _, err := fmt.Fprintf(t, `<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" to="%s" version="1.0" />`, t.Config.Domain); err != nil {
t.cleanup(websocket.StatusBadGateway)
return "", NewConnError(err, true)
}
sessionID, err := stanza.InitStream(t.GetDecoder())
if err != nil {
t.Close()
return "", NewConnError(err, false)
}
return sessionID, nil
}
// startReader runs a go function that keeps reading from the websocket. This
// is required to allow Ping() to work: Ping requires a Reader to be running
// to process incoming control frames.
func (t WebsocketTransport) startReader() {
go func() {
buffer := make([]byte, maxPacketSize)
for {
_, reader, err := t.wsConn.Reader(t.closeCtx)
if err != nil {
return
}
n, err := reader.Read(buffer)
if err != nil && err != io.EOF {
return
}
if n > 0 {
// We need to make a copy, otherwise we will overwrite the slice content
// on the next iteration of the for loop.
tmp := make([]byte, n)
copy(tmp, buffer)
t.queue <- tmp
}
}
}()
}
func (t WebsocketTransport) StartTLS() error {
return ErrTLSNotSupported
}
func (t WebsocketTransport) DoesStartTLS() bool {
return false
}
func (t WebsocketTransport) GetDomain() string {
return t.Config.Domain
}
func (t WebsocketTransport) GetDecoder() *xml.Decoder {
return t.decoder
}
func (t WebsocketTransport) IsSecure() bool {
return strings.HasPrefix(t.Config.Address, "wss:")
}
func (t WebsocketTransport) Ping() error {
ctx, cancel := context.WithTimeout(t.closeCtx, pingTimeout)
defer cancel()
return t.wsConn.Ping(ctx)
}
func (t *WebsocketTransport) Read(p []byte) (int, error) {
select {
case <-t.closeCtx.Done():
return 0, t.closeCtx.Err()
case data := <-t.queue:
if t.logFile != nil && len(data) > 0 {
_, _ = fmt.Fprintf(t.logFile, "RECV:\n%s\n\n", data)
}
copy(p, data)
return len(data), nil
}
}
func (t WebsocketTransport) Write(p []byte) (int, error) {
if t.logFile != nil {
_, _ = fmt.Fprintf(t.logFile, "SEND:\n%s\n\n", p)
}
return len(p), t.wsConn.Write(t.closeCtx, websocket.MessageText, p)
}
func (t WebsocketTransport) Close() error {
t.Write([]byte("<close xmlns=\"urn:ietf:params:xml:ns:xmpp-framing\" />"))
return t.cleanup(websocket.StatusGoingAway)
}
func (t *WebsocketTransport) LogTraffic(logFile io.Writer) {
t.logFile = logFile
}
func (t *WebsocketTransport) cleanup(code websocket.StatusCode) error {
var err error
if t.queue != nil {
close(t.queue)
t.queue = nil
}
if t.wsConn != nil {
err = t.wsConn.Close(websocket.StatusGoingAway, "Done")
t.wsConn = nil
}
if t.closeFunc != nil {
t.closeFunc()
t.closeFunc = nil
t.closeCtx = nil
}
return err
}
// ReceivedStreamClose is not used for websockets for now
func (t *WebsocketTransport) ReceivedStreamClose() {
return
}

View File

@@ -1,158 +0,0 @@
package xmpp
import (
"bufio"
"crypto/tls"
"encoding/xml"
"errors"
"fmt"
"io"
"net"
"time"
"gosrc.io/xmpp/stanza"
)
// XMPPTransport implements the XMPP native TCP transport
// The decoder is expected to be initialized after connecting to a server.
type XMPPTransport struct {
openStatement string
Config TransportConfiguration
TLSConfig *tls.Config
decoder *xml.Decoder
conn net.Conn
readWriter io.ReadWriter
logFile io.Writer
isSecure bool
closeChan chan stanza.StreamClosePacket
}
var componentStreamOpen = fmt.Sprintf("<?xml version='1.0'?><stream:stream to='%%s' xmlns='%s' xmlns:stream='%s'>", stanza.NSComponent, stanza.NSStream)
var clientStreamOpen = fmt.Sprintf("<?xml version='1.0'?><stream:stream to='%%s' xmlns='%s' xmlns:stream='%s' version='1.0'>", stanza.NSClient, stanza.NSStream)
func (t *XMPPTransport) Connect() (string, error) {
var err error
t.conn, err = net.DialTimeout("tcp", t.Config.Address, time.Duration(t.Config.ConnectTimeout)*time.Second)
if err != nil {
return "", NewConnError(err, true)
}
t.closeChan = make(chan stanza.StreamClosePacket)
t.readWriter = newStreamLogger(t.conn, t.logFile)
t.decoder = xml.NewDecoder(bufio.NewReaderSize(t.readWriter, maxPacketSize))
t.decoder.CharsetReader = t.Config.CharsetReader
return t.StartStream()
}
func (t *XMPPTransport) StartStream() (string, error) {
if _, err := fmt.Fprintf(t, t.openStatement, t.Config.Domain); err != nil {
t.Close()
return "", NewConnError(err, true)
}
sessionID, err := stanza.InitStream(t.GetDecoder())
if err != nil {
t.Close()
return "", NewConnError(err, false)
}
return sessionID, nil
}
func (t *XMPPTransport) DoesStartTLS() bool {
return true
}
func (t *XMPPTransport) GetDomain() string {
return t.Config.Domain
}
func (t *XMPPTransport) GetDecoder() *xml.Decoder {
return t.decoder
}
func (t *XMPPTransport) IsSecure() bool {
return t.isSecure
}
func (t *XMPPTransport) StartTLS() error {
if t.Config.TLSConfig == nil {
t.TLSConfig = &tls.Config{}
} else {
t.TLSConfig = t.Config.TLSConfig.Clone()
}
if t.TLSConfig.ServerName == "" {
t.TLSConfig.ServerName = t.Config.Domain
}
tlsConn := tls.Client(t.conn, t.TLSConfig)
// We convert existing connection to TLS
if err := tlsConn.Handshake(); err != nil {
return err
}
t.conn = tlsConn
t.readWriter = newStreamLogger(tlsConn, t.logFile)
t.decoder = xml.NewDecoder(bufio.NewReaderSize(t.readWriter, maxPacketSize))
t.decoder.CharsetReader = t.Config.CharsetReader
if !t.TLSConfig.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(t.Config.Domain); err != nil {
return err
}
}
t.isSecure = true
return nil
}
func (t *XMPPTransport) Ping() error {
n, err := t.conn.Write([]byte("\n"))
if err != nil {
return err
}
if n != 1 {
return errors.New("Could not write ping")
}
return nil
}
func (t *XMPPTransport) Read(p []byte) (n int, err error) {
if t.readWriter == nil {
return 0, errors.New("cannot read: not connected, no readwriter")
}
return t.readWriter.Read(p)
}
func (t *XMPPTransport) Write(p []byte) (n int, err error) {
if t.readWriter == nil {
return 0, errors.New("cannot write: not connected, no readwriter")
}
return t.readWriter.Write(p)
}
func (t *XMPPTransport) Close() error {
if t.readWriter != nil {
_, _ = t.readWriter.Write([]byte(stanza.StreamClose))
}
// Try to wait for the stream close tag from the server. After a timeout, disconnect anyway.
select {
case <-t.closeChan:
case <-time.After(time.Duration(t.Config.ConnectTimeout) * time.Second):
}
if t.conn != nil {
return t.conn.Close()
}
return nil
}
func (t *XMPPTransport) LogTraffic(logFile io.Writer) {
t.logFile = logFile
}
func (t *XMPPTransport) ReceivedStreamClose() {
t.closeChan <- stanza.StreamClosePacket{}
}