mirror of
https://github.com/FluuxIO/go-xmpp.git
synced 2025-11-06 18:23:43 -08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2781563ea7 | ||
|
|
4f68c5eee2 | ||
|
|
9c8353d081 | ||
|
|
3b66e31888 | ||
|
|
005c8823d9 | ||
|
|
2cdda48467 |
@@ -34,8 +34,8 @@ Here is an example code to configure a client to allow connecting to a server wi
|
||||
config := xmpp.Config{
|
||||
Address: "localhost:5222",
|
||||
Jid: "test@localhost",
|
||||
Password: "test",
|
||||
TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||
Credential: xmpp.Password("Test"),
|
||||
TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -96,7 +96,7 @@ func main() {
|
||||
config := xmpp.Config{
|
||||
Address: "localhost:5222",
|
||||
Jid: "test@localhost",
|
||||
Password: "test",
|
||||
Credential: xmpp.Password("Test"),
|
||||
StreamLogger: os.Stdout,
|
||||
Insecure: true,
|
||||
}
|
||||
|
||||
@@ -4,3 +4,5 @@ github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
|
||||
@@ -17,7 +17,7 @@ func main() {
|
||||
config := xmpp.Config{
|
||||
Address: "localhost:5222",
|
||||
Jid: "test@localhost",
|
||||
Password: "test",
|
||||
Credential: xmpp.Password("test"),
|
||||
StreamLogger: os.Stdout,
|
||||
Insecure: true,
|
||||
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||
@@ -48,6 +48,3 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||
_ = s.Send(reply)
|
||||
}
|
||||
|
||||
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
|
||||
// (using templates ?)
|
||||
|
||||
@@ -32,9 +32,9 @@ func main() {
|
||||
|
||||
// 2. Prepare XMPP client
|
||||
config := xmpp.Config{
|
||||
Address: *address,
|
||||
Jid: *jid,
|
||||
Password: *password,
|
||||
Address: *address,
|
||||
Jid: *jid,
|
||||
Credential: xmpp.Password(*password),
|
||||
// StreamLogger: os.Stdout,
|
||||
Insecure: true,
|
||||
}
|
||||
|
||||
48
_examples/xmpp_oauth2/xmpp_oauth2.go
Normal file
48
_examples/xmpp_oauth2/xmpp_oauth2.go
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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{
|
||||
Address: "localhost:5222",
|
||||
Jid: "test@localhost",
|
||||
Credential: xmpp.OAuthToken("OdAIsBlY83SLBaqQoClAn7vrZSHxixT8"),
|
||||
StreamLogger: os.Stdout,
|
||||
// Insecure: true,
|
||||
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||
// for you automatically.
|
||||
cm := xmpp.NewStreamManager(client, nil)
|
||||
log.Fatal(cm.Run())
|
||||
}
|
||||
|
||||
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||
msg, ok := p.(stanza.Message)
|
||||
if !ok {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||
}
|
||||
64
auth.go
64
auth.go
@@ -10,29 +10,57 @@ import (
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
|
||||
// TODO: Implement other type of SASL Authentication
|
||||
havePlain := false
|
||||
for _, m := range f.Mechanisms.Mechanism {
|
||||
if m == "PLAIN" {
|
||||
havePlain = true
|
||||
// Credential is used to pass the type of secret that will be used to connect to XMPP server.
|
||||
// It can be either a password or an OAuth 2 bearer token.
|
||||
type Credential struct {
|
||||
secret string
|
||||
mechanisms []string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
if !havePlain {
|
||||
err := fmt.Errorf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism)
|
||||
|
||||
switch matchingMech {
|
||||
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 authPlain(socket, decoder, user, password)
|
||||
}
|
||||
|
||||
// Plain authentication: send base64-encoded \x00 user \x00 password
|
||||
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
|
||||
raw := "\x00" + user + "\x00" + password
|
||||
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user string, secret string) error {
|
||||
raw := "\x00" + user + "\x00" + secret
|
||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
|
||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='%s'>%s</auth>", stanza.NSSASL, mech, enc)
|
||||
|
||||
// Next message should be either success or failure.
|
||||
val, err := stanza.NextPacket(decoder)
|
||||
@@ -51,3 +79,13 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
13
client.go
13
client.go
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -110,8 +111,8 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
||||
return nil, NewConnError(err, true)
|
||||
}
|
||||
|
||||
if config.Password == "" {
|
||||
err = errors.New("missing password")
|
||||
if config.Credential.secret == "" {
|
||||
err = errors.New("missing credential")
|
||||
return nil, NewConnError(err, true)
|
||||
}
|
||||
|
||||
@@ -211,7 +212,7 @@ func (c *Client) Send(packet stanza.Packet) error {
|
||||
return errors.New("cannot marshal packet " + err.Error())
|
||||
}
|
||||
|
||||
return c.sendWithLogger(string(data))
|
||||
return c.sendWithWriter(c.Session.streamLogger, data)
|
||||
}
|
||||
|
||||
// SendRaw sends an XMPP stanza as a string to the server.
|
||||
@@ -224,12 +225,12 @@ func (c *Client) SendRaw(packet string) error {
|
||||
return errors.New("client is not connected")
|
||||
}
|
||||
|
||||
return c.sendWithLogger(packet)
|
||||
return c.sendWithWriter(c.Session.streamLogger, []byte(packet))
|
||||
}
|
||||
|
||||
func (c *Client) sendWithLogger(packet string) error {
|
||||
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||
var err error
|
||||
_, err = fmt.Fprintf(c.Session.streamLogger, packet)
|
||||
_, err = writer.Write(packet)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
19
client_internal_test.go
Normal file
19
client_internal_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func TestClient_Connect(t *testing.T) {
|
||||
mock.Start(t, testXMPPAddress, handlerConnectSuccess)
|
||||
|
||||
// Test / Check result
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test"), Insecure: true}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
@@ -47,7 +47,7 @@ func TestClient_NoInsecure(t *testing.T) {
|
||||
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
||||
|
||||
// Test / Check result
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test")}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
@@ -71,7 +71,7 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
||||
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
||||
|
||||
// Test / Check result
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test")}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
@@ -94,7 +94,7 @@ func TestClient_RFC3921Session(t *testing.T) {
|
||||
mock.Start(t, testXMPPAddress, handlerConnectWithSession)
|
||||
|
||||
// Test / Check result
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
|
||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test"), Insecure: true}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
|
||||
@@ -32,9 +32,9 @@ func sendxmpp(cmd *cobra.Command, args []string) {
|
||||
|
||||
var err error
|
||||
client, err := xmpp.NewClient(xmpp.Config{
|
||||
Jid: viper.GetString("jid"),
|
||||
Address: viper.GetString("addr"),
|
||||
Password: viper.GetString("password"),
|
||||
Jid: viper.GetString("jid"),
|
||||
Address: viper.GetString("addr"),
|
||||
Credential: xmpp.Password(viper.GetString("password")),
|
||||
}, xmpp.NewRouter())
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -143,6 +143,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
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=
|
||||
|
||||
@@ -10,7 +10,7 @@ type Config struct {
|
||||
Address string
|
||||
Jid string
|
||||
parsedJid *Jid // For easier manipulation
|
||||
Password string
|
||||
Credential Credential
|
||||
StreamLogger *os.File // Used for debugging
|
||||
Lang string // TODO: should default to 'en'
|
||||
ConnectTimeout int // Client timeout in seconds. Default to 15
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,5 +4,5 @@ go 1.12
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.3.0
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@@ -2,3 +2,5 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
|
||||
@@ -168,7 +168,7 @@ func (s *Session) auth(o Config) {
|
||||
return
|
||||
}
|
||||
|
||||
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
|
||||
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Credential)
|
||||
}
|
||||
|
||||
// Attempt to resume session using stream management
|
||||
|
||||
31
stanza/error_test.go
Normal file
31
stanza/error_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestErr_UnmarshalXML(t *testing.T) {
|
||||
packet := `
|
||||
<iq from='pubsub.example.com'
|
||||
id='kj4vz31m'
|
||||
to='romeo@example.net/foo'
|
||||
type='error'>
|
||||
<error type='wait'>
|
||||
<resource-constraint
|
||||
xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>System overloaded, please retry</text>
|
||||
</error>
|
||||
</iq>`
|
||||
|
||||
parsedIQ := IQ{}
|
||||
data := []byte(packet)
|
||||
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
xmppError := parsedIQ.Error
|
||||
if xmppError.Text != "System overloaded, please retry" {
|
||||
t.Errorf("Could not extract error text: '%s'", xmppError.Text)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,6 @@ func TestControlSet(t *testing.T) {
|
||||
}
|
||||
|
||||
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
|
||||
t.Errorf("Paylod is not an iot control set: %v", cs)
|
||||
t.Errorf("Payload is not an iot control set: %v", cs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type IQ struct { // Info/Query
|
||||
// request."
|
||||
Payload IQPayload `xml:",omitempty"`
|
||||
Error Err `xml:"error,omitempty"`
|
||||
// Any is used to decode unknown payload as a generique structure
|
||||
// Any is used to decode unknown payload as a generic structure
|
||||
Any *Node `xml:",any"`
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import "encoding/xml"
|
||||
type Node struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content string `xml:",innerxml"`
|
||||
Content string `xml:",cdata"`
|
||||
Nodes []Node `xml:",any"`
|
||||
}
|
||||
|
||||
@@ -47,5 +47,8 @@ func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
|
||||
err = e.EncodeToken(start)
|
||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||
if n.Content != "" {
|
||||
e.EncodeToken(xml.CharData(n.Content))
|
||||
}
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
30
stanza/node_test.go
Normal file
30
stanza/node_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNode_Marshal(t *testing.T) {
|
||||
jsonData := []byte("{\"key\":\"value\"}")
|
||||
|
||||
iqResp := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
iqResp.Any = &Node{
|
||||
XMLName: xml.Name{Space: "myNS", Local: "space"},
|
||||
Content: string(jsonData),
|
||||
}
|
||||
|
||||
bytes, err := xml.Marshal(iqResp)
|
||||
if err != nil {
|
||||
t.Errorf("Could not marshal XML: %v", err)
|
||||
}
|
||||
|
||||
parsedIQ := IQ{}
|
||||
if err := xml.Unmarshal(bytes, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal returned error: %v", err)
|
||||
}
|
||||
|
||||
if parsedIQ.Any.Content != string(jsonData) {
|
||||
t.Errorf("Cannot find generic any payload in parsedIQ: '%s'", parsedIQ.Any.Content)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user