XMPP

298 views
Skip to first unread message

goget

unread,
Mar 15, 2010, 10:58:46 AM3/15/10
to golang-nuts
Does anyone know of any XMPP package in Go being developed. If not
some ideas about getting started would be great.

Michael Hoisie

unread,
Mar 15, 2010, 1:22:07 PM3/15/10
to golang-nuts
I don't believe there is. And it's definitely something worth having.

- Mike

Walter Mundt

unread,
Mar 15, 2010, 1:40:41 PM3/15/10
to goget, golang-nuts
Quoting goget <agup...@gmail.com>:

> Does anyone know of any XMPP package in Go being developed. If not
> some ideas about getting started would be great.
>

I started working on that awhile back but got busy.

In order to have XMPP, you need SASL and stringprep/IDN as well. I
have a basic binding for GNU libidn almost done, and have started
experimenting with implementing a SASL library for Go. I looked at
binding some C SASL library, but decided that it would be much better
to have a native implementation with a Go-friendly interface.

It's all very half-baked, but if you're interested in poking around I
can put it up on Github or something.

--
Walter Mundt
walte...@codethink.info

Russ Cox

unread,
Mar 15, 2010, 2:57:02 PM3/15/10
to Walter Mundt, goget, golang-nuts
I worked on the beginning of an xmpp client at one point,
and it worked well enough to talk to Google Talk, but I didn't
exercise it very much. I've included it below but it's
pretty rough.

The sample usage is

talk, err := xmpp.Talk(acct.User, acct.Password);
if err != nil {
log.Exit(err);
}

talk.Send(xmpp.Chat{Remote: "r...@golang.org", Type: "chat", Text:
"Started running."});
for {
chat, err := talk.Recv();
if err != nil {
log.Exit(err);
}
fmt.Println(chat.Remote, chat.Text)
}

If you look at the implementation, I really like that the
xml package is doing all the heavy lifting. The xmpp
package just defines a bunch of data structures that are
a direct translation of the relevant RFCs and then reads
and writes into and out of them.

The code below refers to an openssl wrapping package,
not included. It should be easy to convert it to use
crypto/tls, which didn't exist when I wrote the code.

Russ


// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Simple XMPP client for talking to Google Talk.
//
// TODO(rsc):
// More precise error handling.
// Presence functionality.

// Package xmpp implements a simple Google Talk client
// using the XMPP protocol described in RFC 3920 and RFC 3921.
package xmpp

import (
"bytes"
"encoding/base64"
"fmt"
"io"
"log"
"os"
"reflect"
"strings"
tls "openssl"
"xml"
)

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"
nsClient = "jabber:client"
)

type Conn struct {
tls *tls.Conn // connection to server
jid string // Jabber ID for our connection
p *xml.Parser
}

func Talk(user, passwd string) (*Conn, os.Error) {
// The talk.google.com servers speak either HTTP or XMPP
// depending on what the client says when it connects.
// If we connect to the HTTPS port, the conversation turns
// TLS immediately, so we can avoid the more complex
// TLS negotiation inside the XMPP protocol.
c, err := tls.Dial("talk.google.com:https")
if err != nil {
return nil, err
}
if err = c.StartTLS(); err != nil {
c.Close()
return nil, err
}

// TODO(rsc): Change this to conn = &Conn{tls: c} once 6g can handle it.
// Right now, the fact that tls is a renamed package name makes it behave
// like conn = &Conn{openssl: c}.
conn := new(Conn)
conn.tls = c
if err := conn.init(user, passwd); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}

func (c *Conn) Close() os.Error { return c.tls.Close() }

func (c *Conn) init(user, passwd string) os.Error {
// c.p = xml.NewParser(tee{c.tls, os.Stdout});
c.p = xml.NewParser(c.tls)

a := strings.Split(user, "@", 3)
if len(a) != 2 {
return os.NewError(fmt.Sprintf("invalid user name - want user@domain"))
}
user = a[0]
domain := a[1]

// Declare intent to be a jabber client.
fmt.Fprintf(c.tls, "<?xml version='1.0'?>\n"+
"<stream:stream to='%s' xmlns='%s'\n"+
" xmlns:stream='%s' version='1.0'>\n",
xmlEscape(domain), nsClient, nsStream)

// Server should respond with a stream opening.
se, err := nextStart(c.p)
if err != nil {
return err
}
if se.Name.Space != nsStream || se.Name.Local != "stream" {
return os.NewError(fmt.Sprintf("expected <stream>, got <%s> in %s",
se.Name.Local, se.Name.Space))
}

// Now we're in the stream and can use Unmarshal.
// Next message should be <features> to tell us authentication options.
var f streamFeatures
if err = c.p.Unmarshal(&f, nil); err != nil {
return os.NewError(fmt.Sprintf("unmarshal <features>: %s", err))
}
havePlain := false
for _, m := range f.Mechanisms.Mechanism {
if m == "PLAIN" {
havePlain = true
break
}
}
if !havePlain {
return os.NewError(fmt.Sprintf("PLAIN authentication is not an
option: %v", f.Mechanisms.Mechanism))
}

// Plain authentication: send base64-encoded \x00 user \x00 password.
raw := "\x00" + user + "\x00" + passwd
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(strings.Bytes(raw), enc)
fmt.Fprintf(c.tls, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n",
nsSASL, enc)

// Next message should be either success or failure.
name, val, err := next(c.p)
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 os.NewError("auth failure: " + v.Any.Local)
default:
return os.NewError(fmt.Sprintf("expected <success> or <failure>, got
<%s> in %s", name.Local, name.Space))
}

// Now that we're authenticated, we're supposed to start the stream over again.
// Declare intent to be a jabber client.
fmt.Fprintf(c.tls, "<stream:stream to='%s' xmlns='%s'\n"+
" xmlns:stream='%s' version='1.0'>\n",
xmlEscape(domain), nsClient, nsStream)

// Here comes another <stream> and <features>.
se, err = nextStart(c.p)
if err != nil {
return err
}
if se.Name.Space != nsStream || se.Name.Local != "stream" {
return os.NewError(fmt.Sprintf("expected <stream>, got <%s> in %s",
se.Name.Local, se.Name.Space))
}
if err = c.p.Unmarshal(&f, nil); err != nil {
return os.NewError(fmt.Sprintf("unmarshal <features>: %s", err))
}

// Send IQ message asking to bind to the local user name.
fmt.Fprintf(c.tls, "<iq type='set' id='x'><bind xmlns='%s'/></iq>\n", nsBind)
var iq clientIQ
if err = c.p.Unmarshal(&iq, nil); err != nil {
return os.NewError(fmt.Sprintf("unmarshal <iq>: %s", err))
}
if iq.Bind == nil {
return os.NewError("<iq> result missing <bind>")
}
c.jid = iq.Bind.Jid // our local id

// We're connected and can now receive and send messages.
fmt.Fprintf(c.tls, "<presence xml:lang='en'><show>xa</show><status>I
for one welcome our new codebot overlords.</status></presence>")
return nil
}

type Chat struct {
Remote string
Type string
Text string
}

func (c *Conn) Recv() (chat Chat, err os.Error) {
for {
_, val, err := next(c.p)
if err != nil {
return Chat{}, err
}
if v, ok := val.(*clientMessage); ok {
return Chat{v.From, v.Type, v.Body}, nil
}
}
panic("unreachable")
}

func (c *Conn) Send(chat Chat) {
fmt.Fprintf(c.tls, "<message to='%s' from='%s' type='chat' xml:lang='en'>"+
"<body>%s</body></message>",
xmlEscape(chat.Remote), xmlEscape(c.jid),
xmlEscape(chat.Text))
}


// RFC 3920 C.1 Streams name space

type streamFeatures struct {
XMLName xml.Name "http://etherx.jabber.org/streams features"
StartTLS *tlsStartTLS
Mechanisms *saslMechanisms
Bind *bindBind
Session bool
}

type streamError struct {
XMLName xml.Name "http://etherx.jabber.org/streams error"
Any xml.Name
Text string
}

// RFC 3920 C.3 TLS name space

type tlsStartTLS struct {
XMLName xml.Name ":ietf:params:xml:ns:xmpp-tls starttls"
Required bool
}

type tlsProceed struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-tls proceed"
}

type tlsFailure struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-tls failure"
}

// RFC 3920 C.4 SASL name space

type saslMechanisms struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-sasl mechanisms"
Mechanism []string
}

type saslAuth struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-sasl auth"
Mechanism string "attr"
}

type saslChallenge string

type saslResponse string

type saslAbort struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-sasl abort"
}

type saslSuccess struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-sasl success"
}

type saslFailure struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-sasl failure"
Any xml.Name
}

// RFC 3920 C.5 Resource binding name space

type bindBind struct {
XMLName xml.Name "urn:ietf:params:xml:ns:xmpp-bind bind"
Resource string
Jid string
}

// RFC 3921 B.1 jabber:client

type clientMessage struct {
XMLName xml.Name "jabber:client message"
From string "attr"
Id string "attr"
To string "attr"
Type string "attr" // chat, error, groupchat, headline, or normal

// These should technically be []clientText,
// but string is much more convenient.
Subject string
Body string
Thread string
}

type clientText struct {
Lang string "attr"
Body string "chardata"
}

type clientPresence struct {
XMLName xml.Name "jabber:client presence"
From string "attr"
Id string "attr"
To string "attr"
Type string "attr" // error, probe, subscribe, subscribed,
unavailable, unsubscribe, unsubscribed
Lang string "attr"

Show string // away, chat, dnd, xa
Status string // sb []clientText
Priority string
Error *clientError
}

type clientIQ struct { // info/query
XMLName xml.Name "jabber:client iq"
From string "attr"
Id string "attr"
To string "attr"
Type string "attr" // error, get, result, set
Error *clientError
Bind *bindBind
}

type clientError struct {
XMLName xml.Name "jabber:client error"
Code string "attr"
Type string "attr"
Any xml.Name
Text string
}

// Scan XML token stream to find next StartElement.
func nextStart(p *xml.Parser) (xml.StartElement, os.Error) {
for {
t, err := p.Token()
if err != nil {
log.Exit("token", err)
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
panic("unreachable")
}

// Prototypical nil pointers for specific XML element names.
var proto = map[string]interface{}{
nsStream + " features": (*streamFeatures)(nil),
nsStream + " error": (*streamError)(nil),

nsTLS + " starttls": (*tlsStartTLS)(nil),
nsTLS + " proceed": (*tlsProceed)(nil),
nsTLS + " failure": (*tlsFailure)(nil),

nsSASL + " mechanisms": (*saslMechanisms)(nil),
nsSASL + " challenge": (*saslChallenge)(nil),
nsSASL + " response": (*saslResponse)(nil),
nsSASL + " abort": (*saslAbort)(nil),
nsSASL + " success": (*saslSuccess)(nil),
nsSASL + " failure": (*saslFailure)(nil),

nsBind + " bind": (*bindBind)(nil),

nsClient + " message": (*clientMessage)(nil),
nsClient + " presence": (*clientPresence)(nil),
nsClient + " iq": (*clientIQ)(nil),
nsClient + " error": (*clientError)(nil),
}

// 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.Parser) (xml.Name, interface{}, os.Error) {
// Read start element to find out what type we want.
se, err := nextStart(p)
if err != nil {
return xml.Name{}, nil, err
}
v, ok := proto[se.Name.Space+" "+se.Name.Local]
if !ok {
return xml.Name{}, nil, os.NewError("unexpected XMPP message " +
se.Name.Space + " <" + se.Name.Local + "/>")
}

// The map lookup got us a pointer.
// Put it in an interface and allocate one.
pv := reflect.NewValue(v).(*reflect.PtrValue)
zv := reflect.MakeZero(pv.Type().(*reflect.PtrType).Elem())
pv.PointTo(zv)

// Unmarshal into that storage.
if err = p.Unmarshal(pv.Interface(), &se); err != nil {
return xml.Name{}, nil, err
}
return se.Name, pv.Interface(), err
}

var xmlSpecial = map[byte]string{
'<': "&lt;",
'>': "&gt;",
'"': "&quot;",
'\'': "&apos;",
'&': "&amp;",
}

func xmlEscape(s string) string {
var b bytes.Buffer
for i := 0; i < len(s); i++ {
c := s[i]
if s, ok := xmlSpecial[c]; ok {
b.WriteString(s)
} else {
b.WriteByte(c)
}
}
return b.String()
}

type tee struct {
r io.Reader
w io.Writer
}

func (t tee) Read(p []byte) (n int, err os.Error) {
n, err = t.r.Read(p)
if n > 0 {
t.w.Write(p[0:n])
}
return
}

goget

unread,
Mar 15, 2010, 3:02:00 PM3/15/10
to golang-nuts
@Walter
Agree with you on having a XMPP library on pure Go. It would be great
to have your stuff on Git-hub. It always good to have something to
start with. Thanks.

@Mike
Thanks for your inputs.


On Mar 15, 10:40 pm, Walter Mundt <waltermu...@codethink.info> wrote:

> waltermu...@codethink.info

goget

unread,
Mar 15, 2010, 10:52:58 PM3/15/10
to golang-nuts
@Russ
Thanks for sharing the code. I tried compiling it with "crypto/tls"...
and got the following compilation error :

undefined: tls.Dial

It would be great if can you share the openssl wrapping package, so as
to make it easy converting this code to use "crypto/tls"

> ...
>
> read more »

Russ Cox

unread,
Mar 15, 2010, 11:30:26 PM3/15/10
to goget, golang-nuts
I'd rather put in the effort to make crypto/tls
work than post the half-baked openssl wrapper.
I'll take a look at making tls setup a bit easier.

Russ

Silas

unread,
Apr 23, 2010, 4:47:17 PM4/23/10
to golang-nuts
Has anyone started work on this?
--
Subscription settings: http://groups.google.com/group/golang-nuts/subscribe?hl=en

Russ Cox

unread,
Apr 23, 2010, 4:50:58 PM4/23/10
to Silas, golang-nuts
On Fri, Apr 23, 2010 at 13:47, Silas <si...@sewell.ch> wrote:
> Has anyone started work on this?

I fixed the setup: you can pass nil as the config now
and the library does the right thing as far as configuration
is concerned. Unfortunately, the protocol logic needs
some work too, and no one is working on that.

https://groups.google.com/group/golang-nuts/msg/243700218978fbf4

Russ Cox

unread,
Apr 28, 2010, 4:36:50 AM4/28/10
to Silas, golang-nuts
On Fri, Apr 23, 2010 at 13:47, Silas <si...@sewell.ch> wrote:
> Has anyone started work on this?

crypto/tls should be usable as of the recent release.
Reply all
Reply to author
Forward
0 new messages