# HG changeset patch # User Chris Jones # Date 1391964748 25200 # Node ID b4bd77d58a3e96dbd5280c14666a40a0de7d567d # Parent 626c390682fcd845e86bbc5743e595239010b594# Parent 750bc33ccdda3ca05ff11056d02f0a26bbe8e9a9 Merge. diff -r 626c390682fc -r b4bd77d58a3e README --- 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 . If you use it, I'd appreciate a note letting me know. Bug reports are diff -r 626c390682fc -r b4bd77d58a3e TODO.txt --- 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. diff -r 626c390682fc -r b4bd77d58a3e example/interact.go --- 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 { diff -r 626c390682fc -r b4bd77d58a3e xmpp/layer1.go --- 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 } } diff -r 626c390682fc -r b4bd77d58a3e xmpp/layer2.go --- 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 } } diff -r 626c390682fc -r b4bd77d58a3e xmpp/layer3.go --- 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 diff -r 626c390682fc -r b4bd77d58a3e xmpp/log.go --- 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{}) { -} diff -r 626c390682fc -r b4bd77d58a3e xmpp/roster.go --- 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) diff -r 626c390682fc -r b4bd77d58a3e xmpp/roster_test.go --- 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)) } diff -r 626c390682fc -r b4bd77d58a3e xmpp/sasl.go --- 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 } } diff -r 626c390682fc -r b4bd77d58a3e xmpp/status.go --- 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 { diff -r 626c390682fc -r b4bd77d58a3e xmpp/structs.go --- 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 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{}) } diff -r 626c390682fc -r b4bd77d58a3e xmpp/structs_test.go --- 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: `&