Initial working version of go XMPP library
This commit is contained in:
parent
f237b861bb
commit
c5732bbf1a
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2015, Mickaël Rémond
|
Copyright (c) 2016, ProcessOne
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
79
xmpp/auth.go
Normal file
79
xmpp/auth.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f 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
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !havePlain {
|
||||||
|
return errors.New(fmt.Sprintf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||||
|
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||||
|
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
|
||||||
|
|
||||||
|
// Next message should be either success or failure.
|
||||||
|
name, val, err := next(decoder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *saslSuccess:
|
||||||
|
case *saslFailure:
|
||||||
|
// v.Any is type of sub-element in failure, which gives a description of what failed.
|
||||||
|
return errors.New("auth failure: " + v.Any.Local)
|
||||||
|
default:
|
||||||
|
return errors.New("expected success or failure, got " + name.Local + " in " + name.Space)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMPP Packet Parsing
|
||||||
|
type saslMechanisms struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
||||||
|
Mechanism []string `xml:"mechanism"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type saslSuccess struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type saslFailure struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
||||||
|
Any xml.Name // error reason is a subelement
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type bindBind struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
||||||
|
Resource string
|
||||||
|
Jid string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session is obsolete in RFC 6121.
|
||||||
|
// Added for compliance with RFC 3121.
|
||||||
|
// Remove when ejabberd purely conforms to RFC 6121.
|
||||||
|
type sessionSession struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
||||||
|
optional xml.Name // If it does exist, it mean we are not required to open session
|
||||||
|
}
|
116
xmpp/client.go
Normal file
116
xmpp/client.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
// Store user defined options
|
||||||
|
options Options
|
||||||
|
// Session gather data that can be accessed by users of this library
|
||||||
|
Session *Session
|
||||||
|
// TCP level connection / can be replace by a TLS session after starttls
|
||||||
|
conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Setting up the client / Checking the parameters
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: better options check
|
||||||
|
func NewClient(options Options) (c *Client, err error) {
|
||||||
|
// TODO: If option address is nil, use the Jid domain to compose the address
|
||||||
|
if options.Address, err = checkAddress(options.Address); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Password == "" {
|
||||||
|
err = errors.New("missing password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c = new(Client)
|
||||||
|
c.options = options
|
||||||
|
|
||||||
|
// Parse JID
|
||||||
|
if c.options.parsedJid, err = NewJid(c.options.Jid); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAddress(addr string) (string, error) {
|
||||||
|
var err error
|
||||||
|
hostport := strings.Split(addr, ":")
|
||||||
|
if len(hostport) > 2 {
|
||||||
|
err = errors.New("too many colons in xmpp server address")
|
||||||
|
return addr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address is composed of two parts, we are good
|
||||||
|
if len(hostport) == 2 && hostport[1] != "" {
|
||||||
|
return addr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port was not passed, we append XMPP default port:
|
||||||
|
return strings.Join([]string{hostport[0], "5222"}, ":"), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new connection to a host given as "hostname" or "hostname:port".
|
||||||
|
// 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.
|
||||||
|
func (c *Client) Connect() (*Session, error) {
|
||||||
|
var tcpconn net.Conn
|
||||||
|
var err error
|
||||||
|
if tcpconn, err = net.Dial("tcp", c.options.Address); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn = tcpconn
|
||||||
|
if c.conn, c.Session, err = NewSession(c.conn, c.options); err != nil {
|
||||||
|
return c.Session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're connected and can now receive and send messages.
|
||||||
|
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
||||||
|
fmt.Fprintf(c.Session.socketProxy, "<presence/>")
|
||||||
|
|
||||||
|
return c.Session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) recv(receiver chan<- interface{}) (err error) {
|
||||||
|
for {
|
||||||
|
_, val, err := next(c.Session.decoder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
receiver <- val
|
||||||
|
val = nil
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel allow client to receive / dispatch packets in for range loop
|
||||||
|
func (c *Client) Recv() <-chan interface{} {
|
||||||
|
ch := make(chan interface{})
|
||||||
|
go c.recv(ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends message text.
|
||||||
|
func (c *Client) Send(packet string) error {
|
||||||
|
fmt.Fprintf(c.Session.socketProxy, packet)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func xmlEscape(s string) string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
xml.Escape(&b, []byte(s))
|
||||||
|
return b.String()
|
||||||
|
}
|
11
xmpp/iq.go
Normal file
11
xmpp/iq.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
type clientIQ struct { // info/query
|
||||||
|
XMLName xml.Name `xml:"jabber:client iq"`
|
||||||
|
Packet
|
||||||
|
Bind bindBind
|
||||||
|
// TODO We need to support detecting the IQ namespace / Query packet
|
||||||
|
// Error clientError
|
||||||
|
}
|
34
xmpp/jid.go
Normal file
34
xmpp/jid.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Jid struct {
|
||||||
|
username string
|
||||||
|
domain string
|
||||||
|
resource string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJid(sjid string) (jid *Jid, err error) {
|
||||||
|
s1 := strings.Split(sjid, "@")
|
||||||
|
if len(s1) != 2 {
|
||||||
|
err = errors.New("invalid JID: " + sjid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jid = new(Jid)
|
||||||
|
jid.username = s1[0]
|
||||||
|
|
||||||
|
s2 := strings.Split(s1[1], "/")
|
||||||
|
if len(s2) > 2 {
|
||||||
|
err = errors.New("invalid JID: " + sjid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jid.domain = s2[0]
|
||||||
|
if len(s2) == 2 {
|
||||||
|
jid.resource = s2[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
24
xmpp/message.go
Normal file
24
xmpp/message.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// XMPP Packet Parsing
|
||||||
|
type ClientMessage struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:client message"`
|
||||||
|
Packet
|
||||||
|
Subject string `xml:"subject,omitempty"`
|
||||||
|
Body string `xml:"body,omitempty"`
|
||||||
|
Thread string `xml:"thread,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Func new message to create an empty message structure without the XML tag matching elements
|
||||||
|
|
||||||
|
func (message *ClientMessage) XMPPFormat() string {
|
||||||
|
return fmt.Sprintf("<message to='%s' type='chat' xml:lang='en'>"+
|
||||||
|
"<body>%s</body></message>",
|
||||||
|
message.To,
|
||||||
|
xmlEscape(message.Body))
|
||||||
|
}
|
10
xmpp/ns.go
Normal file
10
xmpp/ns.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
const (
|
||||||
|
nsStream = "http://etherx.jabber.org/streams"
|
||||||
|
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
||||||
|
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||||
|
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||||
|
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||||
|
nsClient = "jabber:client"
|
||||||
|
)
|
12
xmpp/options.go
Normal file
12
xmpp/options.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Address string
|
||||||
|
Jid string
|
||||||
|
parsedJid *Jid // For easier manipulation
|
||||||
|
Password string
|
||||||
|
PacketLogger *os.File // Used for debugging
|
||||||
|
Lang string // TODO: should default to 'en'
|
||||||
|
}
|
13
xmpp/packet.go
Normal file
13
xmpp/packet.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
type Packet struct {
|
||||||
|
Id string `xml:"id,attr,omitempty"`
|
||||||
|
From string `xml:"from,attr,omitempty"`
|
||||||
|
To string `xml:"to,attr,omitempty"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
Lang string `xml:"lang,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type packetFormatter interface {
|
||||||
|
XMPPFormat() string
|
||||||
|
}
|
96
xmpp/parser.go
Normal file
96
xmpp/parser.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reads and checks the opening XMPP stream element.
|
||||||
|
// It returns a stream structure containing:
|
||||||
|
// - Host: You can check the host against the host you were expecting to connect to
|
||||||
|
// - Id: the Stream ID is a temporary shared secret used for some hash calculation. It is also used by ProcessOne
|
||||||
|
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
|
||||||
|
// getting through the authentication process.
|
||||||
|
func initDecoder(p *xml.Decoder) (sessionID string, err error) {
|
||||||
|
for {
|
||||||
|
var t xml.Token
|
||||||
|
t, err = p.Token()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch elem := t.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
if elem.Name.Space != nsStream || elem.Name.Local != "stream" {
|
||||||
|
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Stream attributes
|
||||||
|
for _, attrs := range elem.Attr {
|
||||||
|
switch attrs.Name.Local {
|
||||||
|
case "id":
|
||||||
|
sessionID = attrs.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan XML token stream to find next StartElement.
|
||||||
|
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||||
|
for {
|
||||||
|
t, err := p.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
return xml.StartElement{}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("token:", err)
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan XML token stream for next element and save into val.
|
||||||
|
// If val == nil, allocate new element based on proto map.
|
||||||
|
// Either way, return val.
|
||||||
|
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
|
||||||
|
// Read start element to find out what type we want.
|
||||||
|
se, err := nextStart(p)
|
||||||
|
if err != nil {
|
||||||
|
return xml.Name{}, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put it in an interface and allocate one.
|
||||||
|
var nv interface{}
|
||||||
|
switch se.Name.Space + " " + se.Name.Local {
|
||||||
|
// TODO: general case = Parse IQ / presence / message => split SASL case
|
||||||
|
case nsSASL + " success":
|
||||||
|
nv = &saslSuccess{}
|
||||||
|
case nsSASL + " failure":
|
||||||
|
nv = &saslFailure{}
|
||||||
|
case nsClient + " message":
|
||||||
|
nv = &ClientMessage{}
|
||||||
|
case nsClient + " presence":
|
||||||
|
nv = &clientPresence{}
|
||||||
|
case nsClient + " iq":
|
||||||
|
nv = &clientIQ{}
|
||||||
|
default:
|
||||||
|
return xml.Name{}, nil, errors.New("unexpected XMPP message " +
|
||||||
|
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode element into pointer storage
|
||||||
|
if err = p.DecodeElement(nv, &se); err != nil {
|
||||||
|
return xml.Name{}, nil, err
|
||||||
|
}
|
||||||
|
return se.Name, nv, err
|
||||||
|
}
|
13
xmpp/presence.go
Normal file
13
xmpp/presence.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// XMPP Packet Parsing
|
||||||
|
type clientPresence struct {
|
||||||
|
XMLName xml.Name `xml:"jabber:client presence"`
|
||||||
|
Packet
|
||||||
|
Show string `xml:"show,attr,omitempty"` // away, chat, dnd, xa
|
||||||
|
Status string `xml:"status,attr,omitempty"`
|
||||||
|
Priority string `xml:"priority,attr,omitempty"`
|
||||||
|
//Error *clientError
|
||||||
|
}
|
178
xmpp/session.go
Normal file
178
xmpp/session.go
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
// Session info
|
||||||
|
BindJid string // Jabber ID as provided by XMPP server
|
||||||
|
StreamId string
|
||||||
|
Features streamFeatures
|
||||||
|
TlsEnabled bool
|
||||||
|
lastPacketId int
|
||||||
|
|
||||||
|
// Session interface
|
||||||
|
In chan interface{}
|
||||||
|
Out chan interface{}
|
||||||
|
|
||||||
|
// read / write
|
||||||
|
socketProxy io.ReadWriter
|
||||||
|
decoder *xml.Decoder
|
||||||
|
|
||||||
|
// error management
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(conn net.Conn, o Options) (net.Conn, *Session, error) {
|
||||||
|
s := new(Session)
|
||||||
|
s.init(conn, o)
|
||||||
|
|
||||||
|
// starttls
|
||||||
|
var tlsConn net.Conn
|
||||||
|
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.domain)
|
||||||
|
s.reset(conn, tlsConn, o)
|
||||||
|
|
||||||
|
// auth
|
||||||
|
s.auth(o)
|
||||||
|
s.reset(tlsConn, tlsConn, o)
|
||||||
|
|
||||||
|
// bind resource and 'start' XMPP session
|
||||||
|
s.bind(o)
|
||||||
|
s.rfc3921Session(o)
|
||||||
|
|
||||||
|
return tlsConn, s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) PacketId() string {
|
||||||
|
s.lastPacketId++
|
||||||
|
return fmt.Sprintf("%x", s.lastPacketId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) init(conn net.Conn, o Options) {
|
||||||
|
s.setProxy(nil, conn, o)
|
||||||
|
s.Features = s.open(o.parsedJid.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) reset(conn net.Conn, newConn net.Conn, o Options) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.setProxy(conn, newConn, o)
|
||||||
|
s.Features = s.open(o.parsedJid.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: setProxyLogger ? better name ? This is not a TCP / HTTP proxy
|
||||||
|
func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Options) {
|
||||||
|
if newConn != conn {
|
||||||
|
s.socketProxy = newSocketProxy(newConn, o.PacketLogger)
|
||||||
|
}
|
||||||
|
s.decoder = xml.NewDecoder(s.socketProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) open(domain string) (f streamFeatures) {
|
||||||
|
// Send stream open tag
|
||||||
|
if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, nsClient, nsStream); s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set xml decoder and extract streamID from reply
|
||||||
|
s.StreamId, s.err = initDecoder(s.decoder) // TODO refactor / rename
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract stream features
|
||||||
|
if s.err = s.decoder.Decode(&f); s.err != nil {
|
||||||
|
s.err = errors.New("stream open decode features: " + s.err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
||||||
|
if s.err != nil {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Features.StartTLS.XMLName.Space+" "+s.Features.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
||||||
|
fmt.Fprintf(s.socketProxy, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
|
||||||
|
var k tlsProceed
|
||||||
|
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
|
||||||
|
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
s.TlsEnabled = true
|
||||||
|
|
||||||
|
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
|
||||||
|
DefaultTlsConfig.ServerName = domain
|
||||||
|
var tlsConn *tls.Conn = tls.Client(conn, &DefaultTlsConfig)
|
||||||
|
// We convert existing connection to TLS
|
||||||
|
if s.err = tlsConn.Handshake(); s.err != nil {
|
||||||
|
return tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// We check that cert matches hostname
|
||||||
|
s.err = tlsConn.VerifyHostname(domain)
|
||||||
|
return tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// starttls is not supported => we do not upgrade the connection:
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) auth(o Options) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.err = authSASL(s.socketProxy, s.decoder, s.Features, o.parsedJid.username, o.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) bind(o Options) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send IQ message asking to bind to the local user name.
|
||||||
|
var resource = o.parsedJid.resource
|
||||||
|
if resource != "" {
|
||||||
|
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
||||||
|
s.PacketId(), nsBind, resource)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), nsBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
var iq clientIQ
|
||||||
|
if s.err = s.decoder.Decode(&iq); s.err != nil || &iq.Bind == nil {
|
||||||
|
s.err = errors.New("iq bind result missing: " + s.err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.BindJid = iq.Bind.Jid // our local id (with possibly randomly generated resource
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove when ejabberd is fixed: https://github.com/processone/ejabberd/issues/869
|
||||||
|
// After the bind, if the session is required (as per old RFC 3921), we send the session open iq
|
||||||
|
func (s *Session) rfc3921Session(o Options) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var iq clientIQ
|
||||||
|
|
||||||
|
// TODO: Do no send unconditionally, check if session is optional and omit it
|
||||||
|
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), nsSession)
|
||||||
|
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
||||||
|
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
49
xmpp/socket_proxy.go
Normal file
49
xmpp/socket_proxy.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mediated Read / Write on socket
|
||||||
|
// Used if logFile from Options is not nil
|
||||||
|
type socketProxy struct {
|
||||||
|
socket io.ReadWriter // Actual connection
|
||||||
|
logFile *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSocketProxy(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
|
||||||
|
if logFile == nil {
|
||||||
|
return conn
|
||||||
|
} else {
|
||||||
|
return &socketProxy{conn, logFile}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *socketProxy) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = pl.socket.Read(p)
|
||||||
|
if n > 0 {
|
||||||
|
pl.logFile.Write([]byte("RECV:\n")) // Prefix
|
||||||
|
if n, err := pl.logFile.Write(p[:n]); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
pl.logFile.Write([]byte("\n\n")) // Separator
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *socketProxy) Write(p []byte) (n int, err error) {
|
||||||
|
pl.logFile.Write([]byte("SEND:\n")) // Prefix
|
||||||
|
for _, w := range []io.Writer{pl.socket, pl.logFile} {
|
||||||
|
n, err = w.Write(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n != len(p) {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pl.logFile.Write([]byte("\n\n")) // Separator
|
||||||
|
return len(p), nil
|
||||||
|
}
|
22
xmpp/starttls.go
Normal file
22
xmpp/starttls.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultTlsConfig tls.Config
|
||||||
|
|
||||||
|
// XMPP Packet Parsing
|
||||||
|
type tlsStartTLS struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
||||||
|
Required bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type tlsProceed struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tlsFailure struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
|
||||||
|
}
|
22
xmpp/stream.go
Normal file
22
xmpp/stream.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// XMPP Packet Parsing
|
||||||
|
type streamFeatures struct {
|
||||||
|
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
||||||
|
StartTLS tlsStartTLS
|
||||||
|
Caps Caps
|
||||||
|
Mechanisms saslMechanisms
|
||||||
|
Bind bindBind
|
||||||
|
Session sessionSession
|
||||||
|
Any []xml.Name `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
47
xmpp_client.go
Normal file
47
xmpp_client.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
xmpp_client is a demo client that connect on an XMPP server and echo message received back to original sender.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mremond/gox/xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
options := xmpp.Options{Address: "localhost:5222", Jid: "test@localhost", Password: "test", PacketLogger: os.Stdout}
|
||||||
|
|
||||||
|
var client *xmpp.Client
|
||||||
|
var err error
|
||||||
|
if client, err = xmpp.NewClient(options); err != nil {
|
||||||
|
log.Fatal("Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var session *xmpp.Session
|
||||||
|
if session, err = client.Connect(); err != nil {
|
||||||
|
log.Fatal("Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Stream opened, we have streamID = ", session.StreamId)
|
||||||
|
|
||||||
|
// Iterator to receive packets coming from our XMPP connection
|
||||||
|
for packet := range client.Recv() {
|
||||||
|
switch packet := packet.(type) {
|
||||||
|
case *xmpp.ClientMessage:
|
||||||
|
fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
|
||||||
|
reply := xmpp.ClientMessage{Packet: xmpp.Packet{To: packet.From}, Body: packet.Body}
|
||||||
|
client.Send(reply.XMPPFormat())
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
|
||||||
|
// (using templates ?)
|
||||||
|
|
||||||
|
// TODO: autoreconnect when connection is lost
|
Loading…
Reference in a new issue