diff -r 69c5b4382e39 -r bbd4166df95d xmpp/xmpp.go --- a/xmpp/xmpp.go Sun Sep 22 17:43:34 2013 -0500 +++ b/xmpp/xmpp.go Sat Sep 28 13:02:17 2013 -0600 @@ -36,20 +36,36 @@ clientSrv = "xmpp-client" ) -// Flow control for preventing sending stanzas until negotiation has -// completed. -type sendCmd int +// Status of the connection. +type Status int const ( - sendAllowConst = iota - sendDenyConst - sendAbortConst + statusUnconnected = iota + statusConnected + statusConnectedTls + statusAuthenticated + statusBound + statusRunning + statusShutdown ) var ( - sendAllow sendCmd = sendAllowConst - sendDeny sendCmd = sendDenyConst - sendAbort sendCmd = sendAbortConst + // 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 @@ -74,14 +90,12 @@ // The client in a client-server XMPP connection. type Client struct { - // This client's JID. This will be updated asynchronously by - // the time StartSession() returns. + // This client's full JID, including resource Jid JID password string saslExpected string authDone bool handlers chan *callback - inputControl chan sendCmd // Incoming XMPP stanzas from the remote will be published on // this channel. Information which is used by this library to // set up the XMPP stream will not appear here. @@ -90,34 +104,51 @@ // channel. Send chan<- Stanza sendXml 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 // this JID is known to. Roster Roster - // Features advertised by the remote. This will be updated - // asynchronously as new features are received throughout the - // connection process. It should not be updated once - // StartSession() returns. + // Features advertised by the remote. Features *Features sendFilterAdd, recvFilterAdd chan Filter - // Allows the user to override the TLS configuration. - tlsConfig tls.Config - layer1 *layer1 + tlsConfig tls.Config + layer1 *layer1 } -// Connect to the appropriate server and authenticate as the given JID -// with the given password. This function will return as soon as a TCP -// connection has been established, but before XMPP stream negotiation -// has completed. The negotiation will occur asynchronously, and any -// send operation to Client.Send will block until negotiation -// (resource binding) is complete. The caller must immediately start -// reading from Client.Recv. -func NewClient(jid *JID, password string, tlsconf tls.Config, exts []Extension) (*Client, error) { +// Creates an XMPP client identified by the given JID, authenticating +// with the provided password and TLS config. Zero or more extensions +// may be specified. The initial presence will be broadcast. If status +// is non-nil, connection progress information will be sent on it. +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) if err != nil { @@ -145,32 +176,13 @@ if tcp == nil { return nil, err } - - cl := new(Client) - cl.Roster = *roster - cl.password = password - cl.Jid = *jid - cl.handlers = make(chan *callback, 100) - cl.inputControl = make(chan sendCmd) - cl.tlsConfig = tlsconf - cl.sendFilterAdd = make(chan Filter) - cl.recvFilterAdd = make(chan Filter) - - 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 - } - } + 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 = startLayer1(tcp, recvWriter, sendReader, + cl.statmgr.newListener()) // Start the reader and writer that convert to and from XML. recvXmlCh := make(chan interface{}) @@ -182,12 +194,12 @@ // Start the reader and writer that convert between XML and // XMPP stanzas. recvRawXmpp := make(chan Stanza) - go cl.recvStream(recvXmlCh, recvRawXmpp) + go cl.recvStream(recvXmlCh, recvRawXmpp, cl.statmgr.newListener()) sendRawXmpp := make(chan Stanza) - go sendStream(sendXmlCh, sendRawXmpp, cl.inputControl) + go sendStream(sendXmlCh, sendRawXmpp, cl.statmgr.newListener()) - // Start the manager for the filters that can modify what the - // app sees. + // Start the managers for the filters that can modify what the + // app sees or sends. recvFiltXmpp := make(chan Stanza) cl.Recv = recvFiltXmpp go filterMgr(cl.recvFilterAdd, recvRawXmpp, recvFiltXmpp) @@ -204,6 +216,44 @@ hsOut := &stream{To: jid.Domain, Version: XMPPVersion} cl.sendXml <- hsOut + // Wait until resource binding is complete. + if err := cl.statmgr.awaitStatus(StatusBound); err != nil { + return nil, err + } + + // Initialize the session. + id := NextId() + iq := &Iq{Header: Header{To: 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") + } + if iq.Type == "error" { + Warn.Logf("Can't start session: %v", iq) + ch <- iq.Error + } + ch <- nil + } + cl.SetCallback(id, f) + cl.sendXml <- iq + // Now wait until the callback is called. + if err := <-ch; err != nil { + return nil, err + } + + // This allows the client to receive stanzas. + cl.setStatus(StatusRunning) + + // Request the roster. + cl.Roster.update() + + // Send the initial presence. + cl.Send <- &pr + return cl, nil } @@ -236,48 +286,3 @@ Debug.Log(buf) } } - -// bindDone is called when we've finished resource binding (and all -// the negotiations that precede it). Now we can start accepting -// traffic from the app. -func (cl *Client) bindDone() { - cl.inputControl <- sendAllow -} - -// Start an XMPP session. A typical XMPP client should call this -// immediately after creating the Client in order to start the session -// and broadcast an initial presence. The presence can be as simple as -// a newly-initialized Presence struct. See RFC 3921, Section -// 3. After calling this, a normal client should call Roster.Update(). -func (cl *Client) StartSession(pr *Presence) error { - id := NextId() - iq := &Iq{Header: Header{To: 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) bool { - iq, ok := st.(*Iq) - if !ok { - Warn.Log("iq reply not iq; can't start session") - ch <- errors.New("bad session start reply") - return false - } - if iq.Type == "error" { - Warn.Logf("Can't start session: %v", iq) - ch <- iq.Error - return false - } - ch <- nil - return false - } - cl.SetCallback(id, f) - cl.Send <- iq - - // Now wait until the callback is called. - if err := <-ch; err != nil { - return err - } - if pr != nil { - cl.Send <- pr - } - return nil -}