forked from chteufleur/go-xmpp
Basic XMPP client with TLS and SASL.
This commit is contained in:
parent
a76c53f05f
commit
a793114fdf
|
|
@ -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 {}
|
||||||
|
}
|
||||||
|
|
@ -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("<?xml version='1.0'?>\n"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Send stream start.
|
||||||
|
s := fmt.Sprintf(
|
||||||
|
"<stream:stream from='%s' to='%s' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>",
|
||||||
|
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(
|
||||||
|
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>",
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
func saslEncodePlain(user, password string) string {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte("\x00" + user + "\x00" + password))
|
||||||
|
}
|
||||||
|
|
@ -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("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"); 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"`
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue