Merge.
--- a/README Sun Feb 09 09:50:38 2014 -0700
+++ b/README Sun Feb 09 09:52:28 2014 -0700
@@ -4,7 +4,14 @@
The core of the protocol is handled by xmpp.go, structs.go, and
stream.go. Everything else is an extension, though some of the
-provided "extensions" are mandatory pieces of the protocol.
+provided "extensions" are mandatory pieces of the protocol. Many of
+the XEPs at http://xmpp.org/xmpp-protocols/xmpp-extensions/ can be
+supported by this library, though at present only base protocol
+support is here.
+
+An simple client using this library is in the example directory. A
+more interesting example can be found at
+https://cjones.org/hg/foosfiend.
This software is written by Chris Jones <chris@cjones.org>. If you use
it, I'd appreciate a note letting me know. Bug reports are
--- a/TODO.txt Sun Feb 09 09:50:38 2014 -0700
+++ b/TODO.txt Sun Feb 09 09:52:28 2014 -0700
@@ -1,16 +1,2 @@
-Review all the *Client receiver methods in layer3.go. They should
-probably either all be receivers, or none.
-
-Add a Reconnect() function.
-
-Eliminate as many uses of Generic as possible.
-
-Don't keep the password in memory once we're done with it.
-
-Rename extension.StanzaHandlers to something like StanzaTypes.
-
-Think about how to gracefully shutdown. Probably have a Close()
-function.
-
-Get rid of logging. We're providing status updates. Allow some sort of
-debug flag that prints from layer1.
+Don't force the client to understand the RFCs. Keep message types in a
+convenient set of constants, for example.
--- a/example/interact.go Sun Feb 09 09:50:38 2014 -0700
+++ b/example/interact.go Sun Feb 09 09:52:28 2014 -0700
@@ -11,42 +11,34 @@
"strings"
)
-type StdLogger struct {
-}
-
-func (s *StdLogger) Log(v ...interface{}) {
- log.Println(v...)
-}
-
-func (s *StdLogger) Logf(fmt string, v ...interface{}) {
- log.Printf(fmt, v...)
-}
-
func init() {
- logger := &StdLogger{}
- // xmpp.Debug = logger
- xmpp.Info = logger
- xmpp.Warn = logger
+ // xmpp.Debug = true
}
// Demonstrate the API, and allow the user to interact with an XMPP
// server via the terminal.
func main() {
- var jid xmpp.JID
- flag.Var(&jid, "jid", "JID to log in as")
- var pw *string = flag.String("pw", "", "password")
+ jidStr := flag.String("jid", "", "JID to log in as")
+ pw := flag.String("pw", "", "password")
flag.Parse()
- if jid.Domain == "" || *pw == "" {
+ jid := xmpp.JID(*jidStr)
+ if jid.Domain() == "" || *pw == "" {
flag.Usage()
os.Exit(2)
}
+ stat := make(chan xmpp.Status)
+ go func() {
+ for s := range stat {
+ log.Printf("connection status %d", s)
+ }
+ }()
tlsConf := tls.Config{InsecureSkipVerify: true}
- c, err := xmpp.NewClient(&jid, *pw, tlsConf, nil, xmpp.Presence{}, nil)
+ c, err := xmpp.NewClient(&jid, *pw, tlsConf, nil, xmpp.Presence{}, stat)
if err != nil {
log.Fatalf("NewClient(%v): %v", jid, err)
}
- defer close(c.Send)
+ defer c.Close()
go func(ch <-chan xmpp.Stanza) {
for obj := range ch {
--- a/xmpp/layer1.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/layer1.go Sun Feb 09 09:52:28 2014 -0700
@@ -5,11 +5,16 @@
import (
"crypto/tls"
+ "fmt"
"io"
+ "log"
"net"
"time"
)
+// If enabled, print all sent and received XML.
+var Debug = false
+
var l1interval = time.Second
type layer1 struct {
@@ -18,15 +23,15 @@
sendSocks chan net.Conn
}
-func startLayer1(sock net.Conn, recvWriter io.WriteCloser,
+func (cl *Client) startLayer1(sock net.Conn, recvWriter io.WriteCloser,
sendReader io.ReadCloser, status <-chan Status) *layer1 {
l1 := layer1{sock: sock}
recvSocks := make(chan net.Conn)
l1.recvSocks = recvSocks
sendSocks := make(chan net.Conn, 1)
l1.sendSocks = sendSocks
- go recvTransport(recvSocks, recvWriter, status)
- go sendTransport(sendSocks, sendReader)
+ go cl.recvTransport(recvSocks, recvWriter, status)
+ go cl.sendTransport(sendSocks, sendReader)
recvSocks <- sock
sendSocks <- sock
return &l1
@@ -50,7 +55,7 @@
l1.recvSocks <- l1.sock
}
-func recvTransport(socks <-chan net.Conn, w io.WriteCloser,
+func (cl *Client) recvTransport(socks <-chan net.Conn, w io.WriteCloser,
status <-chan Status) {
defer w.Close()
@@ -59,7 +64,7 @@
for {
select {
case stat := <-status:
- if stat == StatusShutdown {
+ if stat.Fatal() {
return
}
@@ -78,27 +83,33 @@
continue
}
}
- Warn.Logf("recvTransport: %s", err)
+ cl.setError(fmt.Errorf("recv: %v", err))
return
}
+ if Debug {
+ log.Printf("recv: %s", p[:nr])
+ }
nw, err := w.Write(p[:nr])
if nw < nr {
- Warn.Logf("recvTransport: %s", err)
+ cl.setError(fmt.Errorf("recv: %v", err))
return
}
}
}
}
-func sendTransport(socks <-chan net.Conn, r io.Reader) {
+func (cl *Client) sendTransport(socks <-chan net.Conn, r io.Reader) {
var sock net.Conn
p := make([]byte, 1024)
for {
nr, err := r.Read(p)
if nr == 0 {
- Warn.Logf("sendTransport: %s", err)
+ cl.setError(fmt.Errorf("send: %v", err))
break
}
+ if nr > 0 && Debug {
+ log.Printf("send: %s", p[:nr])
+ }
for nr > 0 {
select {
case sock = <-socks:
@@ -114,7 +125,7 @@
nw, err := sock.Write(p[:nr])
nr -= nw
if nr != 0 {
- Warn.Logf("write: %s", err)
+ cl.setError(fmt.Errorf("send: %v", err))
break
}
}
--- a/xmpp/layer2.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/layer2.go Sun Feb 09 09:52:28 2014 -0700
@@ -7,19 +7,16 @@
"encoding/xml"
"fmt"
"io"
+ "log"
"reflect"
"strings"
)
// Read bytes from a reader, unmarshal them as XML into structures of
// the appropriate type, and send those structures on a channel.
-func recvXml(r io.Reader, ch chan<- interface{},
+func (cl *Client) recvXml(r io.Reader, ch chan<- interface{},
extStanza map[xml.Name]reflect.Type) {
- if _, ok := Debug.(*noLog); !ok {
- pr, pw := io.Pipe()
- go tee(r, pw, "S: ")
- r = pr
- }
+
defer close(ch)
// This trick loads our namespaces into the parser.
@@ -35,7 +32,7 @@
t, err := p.Token()
if t == nil {
if err != io.EOF {
- Warn.Logf("read: %s", err)
+ cl.setError(fmt.Errorf("recv: %v", err))
}
break
}
@@ -51,7 +48,7 @@
case NsStream + " stream":
st, err := parseStream(se)
if err != nil {
- Warn.Logf("unmarshal stream: %s", err)
+ cl.setError(fmt.Errorf("recv: %v", err))
break Loop
}
ch <- st
@@ -73,14 +70,16 @@
obj = &Presence{}
default:
obj = &Generic{}
- Info.Logf("Ignoring unrecognized: %s %s", se.Name.Space,
- se.Name.Local)
+ if Debug {
+ log.Printf("Ignoring unrecognized: %s %s",
+ se.Name.Space, se.Name.Local)
+ }
}
// Read the complete XML stanza.
err = p.DecodeElement(obj, &se)
if err != nil {
- Warn.Logf("unmarshal: %s", err)
+ cl.setError(fmt.Errorf("recv: %v", err))
break Loop
}
@@ -90,7 +89,7 @@
if st, ok := obj.(Stanza); ok {
err = parseExtended(st.GetHeader(), extStanza)
if err != nil {
- Warn.Logf("ext unmarshal: %s", err)
+ cl.setError(fmt.Errorf("recv: %v", err))
break Loop
}
}
@@ -133,12 +132,7 @@
// Receive structures on a channel, marshal them to XML, and send the
// bytes on a writer.
-func sendXml(w io.Writer, ch <-chan interface{}) {
- if _, ok := Debug.(*noLog); !ok {
- pr, pw := io.Pipe()
- go tee(pr, w, "C: ")
- w = pw
- }
+func (cl *Client) sendXml(w io.Writer, ch <-chan interface{}) {
defer func(w io.Writer) {
if c, ok := w.(io.Closer); ok {
c.Close()
@@ -151,12 +145,13 @@
if st, ok := obj.(*stream); ok {
_, err := w.Write([]byte(st.String()))
if err != nil {
- Warn.Logf("write: %s", err)
+ cl.setError(fmt.Errorf("send: %v", err))
+ break
}
} else {
err := enc.Encode(obj)
if err != nil {
- Warn.Logf("marshal: %s", err)
+ cl.setError(fmt.Errorf("send: %v", err))
break
}
}
--- a/xmpp/layer3.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/layer3.go Sun Feb 09 09:52:28 2014 -0700
@@ -5,6 +5,8 @@
import (
"encoding/xml"
+ "fmt"
+ "log"
)
// Callback to handle a stanza with a particular id.
@@ -26,7 +28,10 @@
var input <-chan Stanza
for {
select {
- case stat := <-status:
+ case stat, ok := <-status:
+ if !ok {
+ return
+ }
switch stat {
default:
input = nil
@@ -38,7 +43,9 @@
return
}
if x == nil {
- Info.Log("Refusing to send nil stanza")
+ if Debug {
+ log.Println("Won't send nil stanza")
+ }
continue
}
sendXml <- x
@@ -74,7 +81,8 @@
case *stream:
// Do nothing.
case *streamError:
- cl.handleStreamError(obj)
+ cl.setError(fmt.Errorf("%#v", obj))
+ return
case *Features:
cl.handleFeatures(obj)
case *starttls:
@@ -92,23 +100,21 @@
sendXmpp <- obj
}
default:
- Warn.Logf("Unhandled non-stanza: %T %#v", x, x)
+ if Debug {
+ log.Printf("Unrecognized input: %T %#v",
+ x, x)
+ }
}
}
}
}
-func (cl *Client) handleStreamError(se *streamError) {
- Info.Logf("Received stream error: %v", se)
- cl.setStatus(StatusShutdown)
-}
-
func (cl *Client) handleFeatures(fe *Features) {
cl.Features = fe
if fe.Starttls != nil {
start := &starttls{XMLName: xml.Name{Space: NsTLS,
Local: "starttls"}}
- cl.sendXml <- start
+ cl.sendRaw <- start
return
}
@@ -130,12 +136,12 @@
// Now re-send the initial handshake message to start the new
// session.
- cl.sendXml <- &stream{To: cl.Jid.Domain, Version: XMPPVersion}
+ cl.sendRaw <- &stream{To: cl.Jid.Domain(), Version: XMPPVersion}
}
// Send a request to bind a resource. RFC 3920, section 7.
func (cl *Client) bind() {
- res := cl.Jid.Resource
+ res := cl.Jid.Resource()
bindReq := &bindIq{}
if res != "" {
bindReq.Resource = &res
@@ -145,10 +151,13 @@
f := func(st Stanza) {
iq, ok := st.(*Iq)
if !ok {
- Warn.Log("non-iq response")
+ cl.setError(fmt.Errorf("non-iq response to bind %#v",
+ st))
+ return
}
if iq.Type == "error" {
- Warn.Log("Resource binding failed")
+ cl.setError(fmt.Errorf("Resource binding failed"))
+ return
}
var bindRepl *bindIq
for _, ele := range iq.Nested {
@@ -158,22 +167,20 @@
}
}
if bindRepl == nil {
- Warn.Logf("Bad bind reply: %#v", iq)
- }
- jidStr := bindRepl.Jid
- if jidStr == nil || *jidStr == "" {
- Warn.Log("Can't bind empty resource")
+ cl.setError(fmt.Errorf("Bad bind reply: %#v", iq))
+ return
}
- jid := new(JID)
- if err := jid.Set(*jidStr); err != nil {
- Warn.Logf("Can't parse JID %s: %s", *jidStr, err)
+ jid := bindRepl.Jid
+ if jid == nil || *jid == "" {
+ cl.setError(fmt.Errorf("empty resource in bind %#v",
+ iq))
+ return
}
- cl.Jid = *jid
- Info.Logf("Bound resource: %s", cl.Jid.String())
+ cl.Jid = JID(*jid)
cl.setStatus(StatusBound)
}
cl.SetCallback(msg.Id, f)
- cl.sendXml <- msg
+ cl.sendRaw <- msg
}
// Register a callback to handle the next XMPP stanza (iq, message, or
--- a/xmpp/log.go Sun Feb 09 09:50:38 2014 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-// Control over logging from the XMPP library.
-
-package xmpp
-
-var (
- // If any of these are non-nil when NewClient() is called,
- // they will be used to log messages of the indicated
- // severity.
- Warn Logger = &noLog{}
- Info Logger = &noLog{}
- Debug Logger = &noLog{}
-)
-
-// Anything implementing Logger can receive log messages from the XMPP
-// library. The default implementation doesn't log anything; it
-// efficiently discards all messages.
-type Logger interface {
- Log(v ...interface{})
- Logf(fmt string, v ...interface{})
-}
-
-type noLog struct {
- flags int
- prefix string
-}
-
-var _ Logger = &noLog{}
-
-func (l *noLog) Log(v ...interface{}) {
-}
-
-func (l *noLog) Logf(fmt string, v ...interface{}) {
-}
--- a/xmpp/roster.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/roster.go Sun Feb 09 09:52:28 2014 -0700
@@ -16,7 +16,7 @@
// See RFC 3921, Section 7.1.
type RosterItem struct {
XMLName xml.Name `xml:"jabber:iq:roster item"`
- Jid string `xml:"jid,attr"`
+ Jid JID `xml:"jid,attr"`
Subscription string `xml:"subscription,attr"`
Name string `xml:"name,attr"`
Group []string
@@ -34,7 +34,7 @@
}
func (r *Roster) rosterMgr(upd <-chan Stanza) {
- roster := make(map[string]RosterItem)
+ roster := make(map[JID]RosterItem)
var snapshot []RosterItem
var get chan<- []RosterItem
for {
@@ -79,6 +79,7 @@
go r.rosterMgr(rosterUpdate)
recv := func(in <-chan Stanza, out chan<- Stanza) {
defer close(out)
+ defer close(rosterUpdate)
for stan := range in {
rosterUpdate <- stan
out <- stan
@@ -103,9 +104,9 @@
func newRosterExt() *Roster {
r := Roster{}
- r.StanzaHandlers = make(map[xml.Name]reflect.Type)
+ r.StanzaTypes = make(map[xml.Name]reflect.Type)
rName := xml.Name{Space: NsRoster, Local: "query"}
- r.StanzaHandlers[rName] = reflect.TypeOf(RosterQuery{})
+ r.StanzaTypes[rName] = reflect.TypeOf(RosterQuery{})
r.RecvFilter, r.SendFilter = r.makeFilters()
r.get = make(chan []RosterItem)
r.toServer = make(chan Stanza)
--- a/xmpp/roster_test.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/roster_test.go Sun Feb 09 09:52:28 2014 -0700
@@ -29,7 +29,7 @@
t.Fatalf("parseExtended: %v", err)
}
assertEquals(t, "iq", iq.XMLName.Local)
- assertEquals(t, "from", iq.From)
+ assertEquals(t, "from", string(iq.From))
assertEquals(t, "en", iq.Lang)
nested := iq.Nested
if nested == nil {
@@ -49,5 +49,5 @@
t.Fatalf("Wrong # items: %v", rq.Item)
}
item := rq.Item[0]
- assertEquals(t, "a@b.c", item.Jid)
+ assertEquals(t, "a@b.c", string(item.Jid))
}
--- a/xmpp/sasl.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/sasl.go Sun Feb 09 09:52:28 2014 -0700
@@ -28,7 +28,7 @@
if digestMd5 {
auth := &auth{XMLName: xml.Name{Space: NsSASL, Local: "auth"},
Mechanism: "DIGEST-MD5"}
- cl.sendXml <- auth
+ cl.sendRaw <- auth
}
}
@@ -39,7 +39,7 @@
b64 := base64.StdEncoding
str, err := b64.DecodeString(srv.Chardata)
if err != nil {
- Warn.Logf("SASL challenge decode: %s", err)
+ cl.setError(fmt.Errorf("SASL: %v", err))
return
}
srvMap := parseSasl(string(str))
@@ -50,13 +50,12 @@
cl.saslDigest2(srvMap)
}
case "failure":
- Info.Log("SASL authentication failed")
+ cl.setError(fmt.Errorf("SASL authentication failed"))
case "success":
cl.setStatus(StatusAuthenticated)
- Info.Log("Sasl authentication succeeded")
cl.Features = nil
- ss := &stream{To: cl.Jid.Domain, Version: XMPPVersion}
- cl.sendXml <- ss
+ ss := &stream{To: cl.Jid.Domain(), Version: XMPPVersion}
+ cl.sendRaw <- ss
}
}
@@ -69,7 +68,7 @@
}
}
if !hasAuth {
- Warn.Log("Server doesn't support SASL auth")
+ cl.setError(fmt.Errorf("Server doesn't support SASL auth"))
return
}
@@ -81,17 +80,17 @@
passwd := cl.password
nonce := srvMap["nonce"]
- digestUri := "xmpp/" + cl.Jid.Domain
+ digestUri := "xmpp/" + cl.Jid.Domain()
nonceCount := int32(1)
nonceCountStr := fmt.Sprintf("%08x", nonceCount)
// Begin building the response. Username is
// user@domain or just domain.
var username string
- if cl.Jid.Node == "" {
- username = cl.Jid.Domain
+ if cl.Jid.Node() == "" {
+ username = cl.Jid.Domain()
} else {
- username = cl.Jid.Node
+ username = cl.Jid.Node()
}
// Generate our own nonce from random data.
@@ -99,7 +98,7 @@
randSize.Lsh(big.NewInt(1), 64)
cnonce, err := rand.Int(rand.Reader, randSize)
if err != nil {
- Warn.Logf("SASL rand: %s", err)
+ cl.setError(fmt.Errorf("SASL rand: %v", err))
return
}
cnonceStr := fmt.Sprintf("%016x", cnonce)
@@ -131,17 +130,17 @@
b64 := base64.StdEncoding
clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "response"},
Chardata: b64.EncodeToString([]byte(clStr))}
- cl.sendXml <- clObj
+ cl.sendRaw <- clObj
}
func (cl *Client) saslDigest2(srvMap map[string]string) {
if cl.saslExpected == srvMap["rspauth"] {
clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "response"}}
- cl.sendXml <- clObj
+ cl.sendRaw <- clObj
} else {
clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "failure"}, Any: &Generic{XMLName: xml.Name{Space: NsSASL,
Local: "abort"}}}
- cl.sendXml <- clObj
+ cl.sendRaw <- clObj
}
}
--- a/xmpp/status.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/status.go Sun Feb 09 09:52:28 2014 -0700
@@ -6,6 +6,53 @@
"fmt"
)
+// Status of the connection.
+type Status int
+
+const (
+ statusUnconnected = iota
+ statusConnected
+ statusConnectedTls
+ statusAuthenticated
+ statusBound
+ statusRunning
+ statusShutdown
+ statusError
+)
+
+var (
+ // The client has not yet connected, or it has been
+ // disconnected from the server.
+ StatusUnconnected Status = statusUnconnected
+ // Initial connection established.
+ StatusConnected Status = statusConnected
+ // Like StatusConnected, but with TLS.
+ StatusConnectedTls Status = statusConnectedTls
+ // Authentication succeeded.
+ StatusAuthenticated Status = statusAuthenticated
+ // Resource binding complete.
+ StatusBound Status = statusBound
+ // Session has started and normal message traffic can be sent
+ // and received.
+ StatusRunning Status = statusRunning
+ // The session has closed, or is in the process of closing.
+ StatusShutdown Status = statusShutdown
+ // The session has encountered an error. Otherwise identical
+ // to StatusShutdown.
+ StatusError Status = statusError
+)
+
+// Does the status value indicate that the client is or has
+// disconnected?
+func (s Status) Fatal() bool {
+ switch s {
+ default:
+ return false
+ case StatusShutdown, StatusError:
+ return true
+ }
+}
+
type statmgr struct {
newStatus chan Status
newlistener chan chan Status
@@ -90,7 +137,7 @@
if current == waitFor {
return nil
}
- if current == StatusShutdown {
+ if current.Fatal() {
break
}
if current > waitFor {
--- a/xmpp/structs.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/structs.go Sun Feb 09 09:52:28 2014 -0700
@@ -5,26 +5,19 @@
import (
"bytes"
"encoding/xml"
- "flag"
"fmt"
- // BUG(cjyar): Doesn't use stringprep. Could try the implementation at
- // "code.google.com/p/go-idn/src/stringprep"
+ "log"
"reflect"
- "regexp"
"strings"
)
+// BUG(cjyar): Doesn't use stringprep. Could try the implementation at
+// "code.google.com/p/go-idn/src/stringprep"
+
// JID represents an entity that can communicate with other
// entities. It looks like node@domain/resource. Node and resource are
// sometimes optional.
-type JID struct {
- Node string
- Domain string
- Resource string
-}
-
-var _ fmt.Stringer = &JID{}
-var _ flag.Value = &JID{}
+type JID string
// XMPP's <stream:stream> XML element
type stream struct {
@@ -82,8 +75,8 @@
// One of the three core XMPP stanza types: iq, message, presence. See
// RFC3920, section 9.
type Header struct {
- To string `xml:"to,attr,omitempty"`
- From string `xml:"from,attr,omitempty"`
+ To JID `xml:"to,attr,omitempty"`
+ From JID `xml:"from,attr,omitempty"`
Id string `xml:"id,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Lang string `xml:"http://www.w3.org/XML/1998/namespace lang,attr,omitempty"`
@@ -96,9 +89,9 @@
type Message struct {
XMLName xml.Name `xml:"jabber:client message"`
Header
- Subject *Generic `xml:"jabber:client subject"`
- Body *Generic `xml:"jabber:client body"`
- Thread *Generic `xml:"jabber:client thread"`
+ Subject []Text `xml:"jabber:client subject"`
+ Body []Text `xml:"jabber:client body"`
+ Thread *Data `xml:"jabber:client thread"`
}
var _ Stanza = &Message{}
@@ -107,9 +100,9 @@
type Presence struct {
XMLName xml.Name `xml:"presence"`
Header
- Show *Generic `xml:"jabber:client show"`
- Status *Generic `xml:"jabber:client status"`
- Priority *Generic `xml:"jabber:client priority"`
+ Show *Data `xml:"jabber:client show"`
+ Status []Text `xml:"jabber:client status"`
+ Priority *Data `xml:"jabber:client priority"`
}
var _ Stanza = &Presence{}
@@ -137,7 +130,23 @@
type bindIq struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource *string `xml:"resource"`
- Jid *string `xml:"jid"`
+ Jid *JID `xml:"jid"`
+}
+
+// Holds human-readable text, with an optional language
+// specification. Generally multiple instances of these can be found
+// together, allowing the software to choose which language to present
+// to the user.
+type Text struct {
+ XMLName xml.Name
+ Lang string `xml:"http://www.w3.org/XML/1998/namespace lang,attr,omitempty"`
+ Chardata string `xml:",chardata"`
+}
+
+// Non-human-readable content of some sort, used by the protocol.
+type Data struct {
+ XMLName xml.Name
+ Chardata string `xml:",chardata"`
}
// Holds an XML element not described by the more specific types.
@@ -149,32 +158,38 @@
var _ fmt.Stringer = &Generic{}
-func (jid *JID) String() string {
- result := jid.Domain
- if jid.Node != "" {
- result = jid.Node + "@" + result
+func (j JID) Node() string {
+ at := strings.Index(string(j), "@")
+ if at == -1 {
+ return ""
}
- if jid.Resource != "" {
- result = result + "/" + jid.Resource
- }
- return result
+ return string(j[:at])
}
-// Set implements flag.Value. It returns true if it successfully
-// parses the string.
-func (jid *JID) Set(val string) error {
- r := regexp.MustCompile("^(([^@/]+)@)?([^@/]+)(/([^@/]+))?$")
- parts := r.FindStringSubmatch(val)
- if parts == nil {
- return fmt.Errorf("%s doesn't match user@domain/resource", val)
+func (j JID) Domain() string {
+ at := strings.Index(string(j), "@")
+ slash := strings.LastIndex(string(j), "/")
+ if slash == -1 {
+ slash = len(j)
}
- // jid.Node = stringprep.Nodeprep(parts[2])
- // jid.Domain = stringprep.Nodeprep(parts[3])
- // jid.Resource = stringprep.Resourceprep(parts[5])
- jid.Node = parts[2]
- jid.Domain = parts[3]
- jid.Resource = parts[5]
- return nil
+ return string(j[at+1 : slash])
+}
+
+func (j JID) Resource() string {
+ slash := strings.LastIndex(string(j), "/")
+ if slash == -1 {
+ return ""
+ }
+ return string(j[slash+1:])
+}
+
+// Returns the bare JID, which is the JID without the resource part.
+func (j JID) Bare() JID {
+ node := j.Node()
+ if node == "" {
+ return JID(j.Domain())
+ }
+ return JID(fmt.Sprintf("%s@%s", node, j.Domain()))
}
func (s *stream) String() string {
@@ -260,7 +275,7 @@
func (er *Error) Error() string {
buf, err := xml.Marshal(er)
if err != nil {
- Warn.Log("double bad error: couldn't marshal error")
+ log.Println("double bad error: couldn't marshal error")
return "unreadable error"
}
return string(buf)
@@ -269,7 +284,7 @@
var bindExt Extension = Extension{}
func init() {
- bindExt.StanzaHandlers = make(map[xml.Name]reflect.Type)
+ bindExt.StanzaTypes = make(map[xml.Name]reflect.Type)
bName := xml.Name{Space: NsBind, Local: "bind"}
- bindExt.StanzaHandlers[bName] = reflect.TypeOf(bindIq{})
+ bindExt.StanzaTypes[bName] = reflect.TypeOf(bindIq{})
}
--- a/xmpp/structs_test.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/structs_test.go Sun Feb 09 09:52:28 2014 -0700
@@ -23,28 +23,19 @@
}
func TestJid(t *testing.T) {
- str := "user@domain/res"
- jid := &JID{}
- if err := jid.Set(str); err != nil {
- t.Errorf("Set(%s) failed: %s", str, err)
- }
- assertEquals(t, "user", jid.Node)
- assertEquals(t, "domain", jid.Domain)
- assertEquals(t, "res", jid.Resource)
- assertEquals(t, str, jid.String())
+ jid := JID("user@domain/res")
+ assertEquals(t, "user", jid.Node())
+ assertEquals(t, "domain", jid.Domain())
+ assertEquals(t, "res", jid.Resource())
- str = "domain.tld"
- if err := jid.Set(str); err != nil {
- t.Errorf("Set(%s) failed: %s", str, err)
+ jid = "domain.tld"
+ if jid.Node() != "" {
+ t.Errorf("Node: %v\n", jid.Node())
}
- if jid.Node != "" {
- t.Errorf("Node: %v\n", jid.Node)
+ assertEquals(t, "domain.tld", jid.Domain())
+ if jid.Resource() != "" {
+ t.Errorf("Resource: %v\n", jid.Resource())
}
- assertEquals(t, "domain.tld", jid.Domain)
- if jid.Resource != "" {
- t.Errorf("Resource: %v\n", jid.Resource)
- }
- assertEquals(t, str, jid.String())
}
func assertMarshal(t *testing.T, expected string, marshal interface{}) {
@@ -108,8 +99,8 @@
}
func TestMarshalEscaping(t *testing.T) {
- msg := &Message{Body: &Generic{XMLName: xml.Name{Local: "body"},
- Chardata: `&<!-- "`}}
+ msg := &Message{Body: []Text{Text{XMLName: xml.Name{Local: "body"},
+ Chardata: `&<!-- "`}}}
exp := `<message xmlns="jabber:client"><body>&<!-- "</body></message>`
assertMarshal(t, exp, msg)
}
@@ -118,12 +109,13 @@
str := `<message to="a@b.c"><body>foo!</body></message>`
r := strings.NewReader(str)
ch := make(chan interface{})
- go recvXml(r, ch, make(map[xml.Name]reflect.Type))
+ cl := &Client{}
+ go cl.recvXml(r, ch, make(map[xml.Name]reflect.Type))
obs := <-ch
exp := &Message{XMLName: xml.Name{Local: "message", Space: "jabber:client"},
Header: Header{To: "a@b.c", Innerxml: "<body>foo!</body>"},
- Body: &Generic{XMLName: xml.Name{Local: "body", Space: "jabber:client"},
- Chardata: "foo!"}}
+ Body: []Text{Text{XMLName: xml.Name{Local: "body", Space: "jabber:client"},
+ Chardata: "foo!"}}}
if !reflect.DeepEqual(obs, exp) {
t.Errorf("read %s\ngot: %#v\nwant: %#v\n", str, obs, exp)
}
--- a/xmpp/xmpp.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/xmpp.go Sun Feb 09 09:52:28 2014 -0700
@@ -7,14 +7,13 @@
package xmpp
import (
- "bytes"
"crypto/tls"
"encoding/xml"
- "errors"
"fmt"
"io"
"net"
"reflect"
+ "sync"
)
const (
@@ -36,38 +35,6 @@
clientSrv = "xmpp-client"
)
-// Status of the connection.
-type Status int
-
-const (
- statusUnconnected = iota
- statusConnected
- statusConnectedTls
- statusAuthenticated
- statusBound
- statusRunning
- statusShutdown
-)
-
-var (
- // The client has not yet connected, or it has been
- // disconnected from the server.
- StatusUnconnected Status = statusUnconnected
- // Initial connection established.
- StatusConnected Status = statusConnected
- // Like StatusConnected, but with TLS.
- StatusConnectedTls Status = statusConnectedTls
- // Authentication succeeded.
- StatusAuthenticated Status = statusAuthenticated
- // Resource binding complete.
- StatusBound Status = statusBound
- // Session has started and normal message traffic can be sent
- // and received.
- StatusRunning Status = statusRunning
- // The session has closed, or is in the process of closing.
- StatusShutdown Status = statusShutdown
-)
-
// A filter can modify the XMPP traffic to or from the remote
// server. It's part of an Extension. The filter function will be
// called in a new goroutine, so it doesn't need to return. The filter
@@ -76,10 +43,9 @@
// Extensions can add stanza filters and/or new XML element types.
type Extension struct {
- // Maps from an XML namespace to a function which constructs a
- // structure to hold the contents of stanzas in that
- // namespace.
- StanzaHandlers map[xml.Name]reflect.Type
+ // Maps from an XML name to a structure which holds stanza
+ // contents with that name.
+ StanzaTypes map[xml.Name]reflect.Type
// If non-nil, will be called once to start the filter
// running. RecvFilter intercepts incoming messages on their
// way from the remote server to the application; SendFilter
@@ -101,9 +67,10 @@
// set up the XMPP stream will not appear here.
Recv <-chan Stanza
// Outgoing XMPP stanzas to the server should be sent to this
- // channel.
+ // channel. The application should not close this channel;
+ // rather, call Close().
Send chan<- Stanza
- sendXml chan<- interface{}
+ sendRaw chan<- interface{}
statmgr *statmgr
// The client's roster is also known as the buddy list. It's
// the set of contacts which are known to this JID, or which
@@ -114,6 +81,8 @@
sendFilterAdd, recvFilterAdd chan Filter
tlsConfig tls.Config
layer1 *layer1
+ error chan error
+ shutdownOnce sync.Once
}
// Creates an XMPP client identified by the given JID, authenticating
@@ -123,34 +92,8 @@
func NewClient(jid *JID, password string, tlsconf tls.Config, exts []Extension,
pr Presence, status chan<- Status) (*Client, error) {
- // Include the mandatory extensions.
- roster := newRosterExt()
- exts = append(exts, roster.Extension)
- exts = append(exts, bindExt)
-
- cl := new(Client)
- cl.Roster = *roster
- cl.password = password
- cl.Jid = *jid
- cl.handlers = make(chan *callback, 100)
- cl.tlsConfig = tlsconf
- cl.sendFilterAdd = make(chan Filter)
- cl.recvFilterAdd = make(chan Filter)
- cl.statmgr = newStatmgr(status)
-
- extStanza := make(map[xml.Name]reflect.Type)
- for _, ext := range exts {
- for k, v := range ext.StanzaHandlers {
- if _, ok := extStanza[k]; ok {
- return nil, fmt.Errorf("duplicate handler %s",
- k)
- }
- extStanza[k] = v
- }
- }
-
// Resolve the domain in the JID.
- _, srvs, err := net.LookupSRV(clientSrv, "tcp", jid.Domain)
+ _, srvs, err := net.LookupSRV(clientSrv, "tcp", jid.Domain())
if err != nil {
return nil, fmt.Errorf("LookupSrv %s: %v", jid.Domain, err)
}
@@ -176,20 +119,75 @@
if tcp == nil {
return nil, err
}
+
+ return newClient(tcp, jid, password, tlsconf, exts, pr, status)
+}
+
+// Connect to the specified host and port. This is otherwise identical
+// to NewClient.
+func NewClientFromHost(jid *JID, password string, tlsconf tls.Config,
+ exts []Extension, pr Presence, status chan<- Status, host string,
+ port int) (*Client, error) {
+
+ addrStr := fmt.Sprintf("%s:%d", host, port)
+ addr, err := net.ResolveTCPAddr("tcp", addrStr)
+ if err != nil {
+ return nil, err
+ }
+ tcp, err := net.DialTCP("tcp", nil, addr)
+ if err != nil {
+ return nil, err
+ }
+
+ return newClient(tcp, jid, password, tlsconf, exts, pr, status)
+}
+
+func newClient(tcp *net.TCPConn, jid *JID, password string, tlsconf tls.Config,
+ exts []Extension, pr Presence, status chan<- Status) (*Client, error) {
+
+ // Include the mandatory extensions.
+ roster := newRosterExt()
+ exts = append(exts, roster.Extension)
+ exts = append(exts, bindExt)
+
+ cl := new(Client)
+ cl.Roster = *roster
+ cl.password = password
+ cl.Jid = *jid
+ cl.handlers = make(chan *callback, 100)
+ cl.tlsConfig = tlsconf
+ cl.sendFilterAdd = make(chan Filter)
+ cl.recvFilterAdd = make(chan Filter)
+ cl.statmgr = newStatmgr(status)
+ cl.error = make(chan error, 1)
+
+ extStanza := make(map[xml.Name]reflect.Type)
+ for _, ext := range exts {
+ for k, v := range ext.StanzaTypes {
+ if _, ok := extStanza[k]; ok {
+ return nil, fmt.Errorf("duplicate handler %s",
+ k)
+ }
+ extStanza[k] = v
+ }
+ }
+
+ // The thing that called this made a TCP connection, so now we
+ // can signal that it's connected.
cl.setStatus(StatusConnected)
// Start the transport handler, initially unencrypted.
recvReader, recvWriter := io.Pipe()
sendReader, sendWriter := io.Pipe()
- cl.layer1 = startLayer1(tcp, recvWriter, sendReader,
+ cl.layer1 = cl.startLayer1(tcp, recvWriter, sendReader,
cl.statmgr.newListener())
// Start the reader and writer that convert to and from XML.
recvXmlCh := make(chan interface{})
- go recvXml(recvReader, recvXmlCh, extStanza)
+ go cl.recvXml(recvReader, recvXmlCh, extStanza)
sendXmlCh := make(chan interface{})
- cl.sendXml = sendXmlCh
- go sendXml(sendWriter, sendXmlCh)
+ cl.sendRaw = sendXmlCh
+ go cl.sendXml(sendWriter, sendXmlCh)
// Start the reader and writer that convert between XML and
// XMPP stanzas.
@@ -213,36 +211,37 @@
}
// Initial handshake.
- hsOut := &stream{To: jid.Domain, Version: XMPPVersion}
- cl.sendXml <- hsOut
+ hsOut := &stream{To: jid.Domain(), Version: XMPPVersion}
+ cl.sendRaw <- hsOut
// Wait until resource binding is complete.
if err := cl.statmgr.awaitStatus(StatusBound); err != nil {
- return nil, err
+ return nil, cl.getError(err)
}
+ // Forget about the password, for paranoia's sake.
+ cl.password = ""
+
// Initialize the session.
id := NextId()
- iq := &Iq{Header: Header{To: cl.Jid.Domain, Id: id, Type: "set",
+ iq := &Iq{Header: Header{To: JID(cl.Jid.Domain()), Id: id, Type: "set",
Nested: []interface{}{Generic{XMLName: xml.Name{Space: NsSession, Local: "session"}}}}}
ch := make(chan error)
f := func(st Stanza) {
iq, ok := st.(*Iq)
if !ok {
- Warn.Log("iq reply not iq; can't start session")
- ch <- errors.New("bad session start reply")
+ ch <- fmt.Errorf("bad session start reply: %#v", st)
}
if iq.Type == "error" {
- Warn.Logf("Can't start session: %v", iq)
- ch <- iq.Error
+ ch <- fmt.Errorf("Can't start session: %v", iq.Error)
}
ch <- nil
}
cl.SetCallback(id, f)
- cl.sendXml <- iq
+ cl.sendRaw <- iq
// Now wait until the callback is called.
if err := <-ch; err != nil {
- return nil, err
+ return nil, cl.getError(err)
}
// This allows the client to receive stanzas.
@@ -254,35 +253,45 @@
// Send the initial presence.
cl.Send <- &pr
- return cl, nil
+ return cl, cl.getError(nil)
+}
+
+func (cl *Client) Close() {
+ // Shuts down the receivers:
+ cl.setStatus(StatusShutdown)
+
+ // Shuts down the senders:
+ cl.shutdownOnce.Do(func() { close(cl.Send) })
}
-func tee(r io.Reader, w io.Writer, prefix string) {
- defer func(w io.Writer) {
- if c, ok := w.(io.Closer); ok {
- c.Close()
- }
- }(w)
-
- buf := bytes.NewBuffer([]uint8(prefix))
- for {
- var c [1]byte
- n, _ := r.Read(c[:])
- if n == 0 {
- break
- }
- n, _ = w.Write(c[:n])
- if n == 0 {
- break
- }
- buf.Write(c[:n])
- if c[0] == '\n' || c[0] == '>' {
- Debug.Log(buf)
- buf = bytes.NewBuffer([]uint8(prefix))
- }
- }
- leftover := buf.String()
- if leftover != "" {
- Debug.Log(buf)
+// If there's a buffered error in the channel, return it. Otherwise,
+// return what was passed to us. The idea is that the error in the
+// channel probably preceded (and caused) the one that's passed as an
+// argument here.
+func (cl *Client) getError(err1 error) error {
+ select {
+ case err0 := <-cl.error:
+ return err0
+ default:
+ return err1
}
}
+
+// Register an error that happened in the internals somewhere. If
+// there's already an error in the channel, discard the newer one in
+// favor of the older.
+func (cl *Client) setError(err error) {
+ defer cl.Close()
+ defer cl.setStatus(StatusError)
+
+ if len(cl.error) > 0 {
+ return
+ }
+ // If we're in a race between two calls to this function,
+ // trying to set the "first" error, just arbitrarily let one
+ // of them win.
+ select {
+ case cl.error <- err:
+ default:
+ }
+}
--- a/xmpp/xmpp_test.go Sun Feb 09 09:50:38 2014 -0700
+++ b/xmpp/xmpp_test.go Sun Feb 09 09:52:28 2014 -0700
@@ -13,7 +13,8 @@
r := strings.NewReader(`<stream:error><bad-foo xmlns="blah"/>` +
`</stream:error>`)
ch := make(chan interface{})
- go recvXml(r, ch, make(map[xml.Name]reflect.Type))
+ cl := &Client{}
+ go cl.recvXml(r, ch, make(map[xml.Name]reflect.Type))
x := <-ch
se, ok := x.(*streamError)
if !ok {
@@ -29,7 +30,7 @@
`<text xml:lang="en" xmlns="` + NsStreams +
`">Error text</text></stream:error>`)
ch = make(chan interface{})
- go recvXml(r, ch, make(map[xml.Name]reflect.Type))
+ go cl.recvXml(r, ch, make(map[xml.Name]reflect.Type))
x = <-ch
se, ok = x.(*streamError)
if !ok {
@@ -47,7 +48,8 @@
`xmlns="` + NsClient + `" xmlns:stream="` + NsStream +
`" version="1.0">`)
ch := make(chan interface{})
- go recvXml(r, ch, make(map[xml.Name]reflect.Type))
+ cl := &Client{}
+ go cl.recvXml(r, ch, make(map[xml.Name]reflect.Type))
x := <-ch
ss, ok := x.(*stream)
if !ok {
@@ -64,9 +66,10 @@
ch := make(chan interface{})
var wg sync.WaitGroup
wg.Add(1)
+ cl := &Client{}
go func() {
defer wg.Done()
- sendXml(w, ch)
+ cl.sendXml(w, ch)
}()
ch <- obj
close(ch)