Merge.
authorChris Jones <christian.jones@sri.com>
Sun, 09 Feb 2014 09:52:28 -0700
changeset 183 b4bd77d58a3e
parent 182 626c390682fc (current diff)
parent 181 750bc33ccdda (diff)
child 184 ce49140fe60b
Merge.
README
TODO.txt
xmpp/log.go
--- 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>&amp;&lt;!-- &#34;</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)