From a793114fdf91be5653b8cd5638bce0bb5279ab48 Mon Sep 17 00:00:00 2001 From: Matt Goodall Date: Wed, 27 Jun 2012 12:05:07 +0100 Subject: [PATCH] Basic XMPP client with TLS and SASL. --- demo.go | 27 +++++++++ src/xmpp/client.go | 145 +++++++++++++++++++++++++++++++++++++++++++++ src/xmpp/jid.go | 45 ++++++++++++++ src/xmpp/sasl.go | 7 +++ src/xmpp/stream.go | 92 ++++++++++++++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 demo.go create mode 100644 src/xmpp/client.go create mode 100644 src/xmpp/jid.go create mode 100644 src/xmpp/sasl.go create mode 100644 src/xmpp/stream.go diff --git a/demo.go b/demo.go new file mode 100644 index 0000000..61b944d --- /dev/null +++ b/demo.go @@ -0,0 +1,27 @@ +package main + +import ( + "flag" + "log" + "xmpp" +) + +func main() { + + jid := flag.String("jid", "", "JID") + password := flag.String("pass", "", "Password") + flag.Parse() + + jid2, err := xmpp.ParseJID(*jid) + if err != nil { + log.Fatal(err) + } + + c, err := xmpp.NewClient(jid2, *password, &xmpp.ClientConfig{}) + if err != nil { + log.Fatal(err) + } + + log.Println(c) + select {} +} diff --git a/src/xmpp/client.go b/src/xmpp/client.go new file mode 100644 index 0000000..0b970f4 --- /dev/null +++ b/src/xmpp/client.go @@ -0,0 +1,145 @@ +package xmpp + +import ( + "crypto/tls" + "encoding/xml" + "errors" + "fmt" + "log" +) + +type Client struct { + JID JID + stream *Stream +} + +type ClientConfig struct { + NoTLS bool + InsecureSkipVerify bool +} + +func NewClient(jid JID, password string, config *ClientConfig) (*Client, error) { + + stream, err := NewStream(jid.Domain + ":5222") + if err != nil { + return nil, err + } + + if err := stream.Send("\n"); err != nil { + return nil, err + } + + for { + // Send stream start. + s := fmt.Sprintf( + "", + jid, + jid.Domain) + if err := stream.Send(fmt.Sprintf(s)); err != nil { + return nil, err + } + + // Receive stream start. + if _, err := stream.Next(&xml.Name{nsStream, "stream"}); err != nil { + return nil, err + } + + // Read features. + f := new(features) + if err := stream.Decode(f); err != nil { + return nil, err + } + + // TLS? + if f.StartTLS != nil && (f.StartTLS.Required != nil || !config.NoTLS) { + tlsConfig := tls.Config{InsecureSkipVerify: config.InsecureSkipVerify} + if err := stream.UpgradeTLS(&tlsConfig); err != nil { + return nil, err + } + continue // Restart + } + + // Authentication + if f.Mechanisms != nil { + log.Println("Authenticating") + if err := authenticate(stream, f.Mechanisms.Mechanisms, jid.Local, password); err != nil { + return nil, err + } + continue // Restart + } + + break + } + + return &Client{jid, stream}, nil +} + +func authenticate(stream *Stream, mechanisms []string, user, password string) error { + + log.Println("authenticate, mechanisms=", mechanisms) + + if !stringSliceContains(mechanisms, "PLAIN") { + return errors.New("Only PLAIN supported for now") + } + + return authenticatePlain(stream, user, password) +} + +func authenticatePlain(stream *Stream, user, password string) error { + + x := fmt.Sprintf( + "%s", + saslEncodePlain(user, password)) + if err := stream.Send(x); err != nil { + return err + } + + if se, err := stream.Next(nil); err != nil { + return err + } else { + if se.Name.Local == "failure" { + f := new(saslFailure) + stream.DecodeElement(f, se) + return errors.New(fmt.Sprintf("Authentication failed: %s", f.Reason.Local)) + } + } + + return nil +} + +func stringSliceContains(l []string, m string) bool { + for _, i := range l { + if i == m { + return true + } + } + return false +} + +type features struct { + XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` + StartTLS *tlsStartTLS `xml:"starttls"` + Mechanisms *mechanisms `xml:"mechanisms"` +} + +type mechanisms struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` + Mechanisms []string `xml:"mechanism"` +} + +type tlsStartTLS struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"` + Required *tlsStartTLSRequired `xml:"required"` +} + +type tlsStartTLSRequired struct { +} + +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"` + Reason xml.Name `xml:",any"` +} diff --git a/src/xmpp/jid.go b/src/xmpp/jid.go new file mode 100644 index 0000000..5b97ead --- /dev/null +++ b/src/xmpp/jid.go @@ -0,0 +1,45 @@ +package xmpp + +import ( + "fmt" + "strings" +) + +type JID struct { + Local string + Domain string + Resource string +} + +func (jid JID) Bare() string { + if jid.Local == "" { + return jid.Domain + } + return fmt.Sprintf("%s@%s", jid.Local, jid.Domain) +} + +func (jid JID) String() string { + if jid.Resource == "" { + return jid.Bare() + } + return fmt.Sprintf("%s@%s/%s", jid.Local, jid.Domain, jid.Resource) +} + +func ParseJID(s string) (jid JID, err error) { + + if parts := strings.SplitN(s, "/", 2); len(parts) == 1 { + s = parts[0] + } else { + s = parts[0] + jid.Resource = parts[1] + } + + if parts := strings.SplitN(s, "@", 2); len(parts) != 2 { + jid.Domain = parts[0] + } else { + jid.Local = parts[0] + jid.Domain = parts[1] + } + + return +} diff --git a/src/xmpp/sasl.go b/src/xmpp/sasl.go new file mode 100644 index 0000000..e2fe106 --- /dev/null +++ b/src/xmpp/sasl.go @@ -0,0 +1,7 @@ +package xmpp + +import "encoding/base64" + +func saslEncodePlain(user, password string) string { + return base64.StdEncoding.EncodeToString([]byte("\x00" + user + "\x00" + password)) +} diff --git a/src/xmpp/stream.go b/src/xmpp/stream.go new file mode 100644 index 0000000..c1acaa6 --- /dev/null +++ b/src/xmpp/stream.go @@ -0,0 +1,92 @@ +package xmpp + +import ( + "crypto/tls" + "encoding/xml" + "errors" + "fmt" + "log" + "net" +) + +const ( + nsStream = "http://etherx.jabber.org/streams" + nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" +) + +type Stream struct { + conn net.Conn + dec *xml.Decoder +} + +func NewStream(addr string) (*Stream, error) { + + log.Println("Connecting to", addr) + + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + + dec := xml.NewDecoder(conn) + return &Stream{conn, dec}, nil +} + +func (stream *Stream) UpgradeTLS(config *tls.Config) error { + + log.Println("Upgrading to TLS") + + if err := stream.Send(""); err != nil { + return err + } + + p := tlsProceed{} + if err := stream.Decode(&p); err != nil { + return err + } + + conn := tls.Client(stream.conn, &tls.Config{InsecureSkipVerify: true}) + if err := conn.Handshake(); err != nil { + return err + } + + stream.conn = conn + stream.dec = xml.NewDecoder(stream.conn) + + return nil +} + +func (stream *Stream) Send(s string) error { + if _, err := stream.conn.Write([]byte(s)); err != nil { + return err + } + return nil +} + +func (stream *Stream) Next(match *xml.Name) (*xml.StartElement, error) { + for { + t, err := stream.dec.Token() + if err != nil { + return nil, err + } + if e, ok := t.(xml.StartElement); ok { + if match != nil && e.Name != *match { + return nil, errors.New(fmt.Sprintf("Expected %s, got %s", *match, e.Name)) + } + return &e, nil + } + } + panic("Unreachable") +} + +func (stream *Stream) Decode(i interface{}) error { + return stream.dec.Decode(i) +} + +func (stream *Stream) DecodeElement(i interface{}, se *xml.StartElement) error { + return stream.dec.DecodeElement(i, se) +} + +type tlsProceed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"` +}