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