diff --git a/_example/example.go b/_example/example.go index 9c01d45..136af5a 100644 --- a/_example/example.go +++ b/_example/example.go @@ -32,7 +32,11 @@ func main() { } flag.Parse() if *username == "" || *password == "" { - flag.Usage() + if *debug && *username == "" && *password == "" { + fmt.Fprintf(os.Stderr, "no username or password were given; attempting ANONYMOUS auth\n") + } else if *username != "" || *password != "" { + flag.Usage() + } } if !*notls { diff --git a/xmpp.go b/xmpp.go index d504e7e..93a983f 100644 --- a/xmpp.go +++ b/xmpp.go @@ -273,11 +273,14 @@ func (c *Client) init(o *Options) error { c.p = xml.NewDecoder(c.conn) } + var domain string a := strings.SplitN(o.User, "@", 2) - if len(a) != 2 { - return errors.New("xmpp: invalid username (want user@domain): " + o.User) - } - domain := a[1] + if len(o.User) > 0 { + if len(a) != 2 { + return errors.New("xmpp: invalid username (want user@domain): " + o.User) + } + domain = a[1] + } // Otherwise, we'll be attempting ANONYMOUS // Declare intent to be a jabber client and gather stream features. f, err := c.startStream(o, domain) @@ -290,88 +293,101 @@ func (c *Client) init(o *Options) error { return err } - // Even digest forms of authentication are unsafe if we do not know that the host - // we are talking to is the actual server, and not a man in the middle playing - // proxy. - if !c.IsEncrypted() && !o.InsecureAllowUnencryptedAuth { - return errors.New("refusing to authenticate over unencrypted TCP connection") - } - - mechanism := "" - for _, m := range f.Mechanisms.Mechanism { - if m == "ANONYMOUS" { - mechanism = m - fmt.Fprintf(c.conn, "\n", nsSASL) - break - } - - a := strings.SplitN(o.User, "@", 2) - if len(a) != 2 { - return errors.New("xmpp: invalid username (want user@domain): " + o.User) - } - user := a[0] - domain := a[1] - - if m == "PLAIN" { - mechanism = m - // Plain authentication: send base64-encoded \x00 user \x00 password. - raw := "\x00" + user + "\x00" + o.Password - enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) - base64.StdEncoding.Encode(enc, []byte(raw)) - fmt.Fprintf(c.conn, "%s\n", nsSASL, enc) - break - } - if m == "DIGEST-MD5" { - mechanism = m - // Digest-MD5 authentication - fmt.Fprintf(c.conn, "\n", nsSASL) - var ch saslChallenge - if err = c.p.DecodeElement(&ch, nil); err != nil { - return errors.New("unmarshal : " + err.Error()) + if o.User == "" && o.Password == "" { + foundAnonymous := false + for _, m := range f.Mechanisms.Mechanism { + if m == "ANONYMOUS" { + fmt.Fprintf(c.conn, "\n", nsSASL) + foundAnonymous = true + break } - b, err := base64.StdEncoding.DecodeString(string(ch)) - if err != nil { - return err + } + if !foundAnonymous { + return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified") + } + } else { + // Even digest forms of authentication are unsafe if we do not know that the host + // we are talking to is the actual server, and not a man in the middle playing + // proxy. + if !c.IsEncrypted() && !o.InsecureAllowUnencryptedAuth { + return errors.New("refusing to authenticate over unencrypted TCP connection") + } + + mechanism := "" + for _, m := range f.Mechanisms.Mechanism { + if m == "ANONYMOUS" { + mechanism = m + fmt.Fprintf(c.conn, "\n", nsSASL) + break } - tokens := map[string]string{} - for _, token := range strings.Split(string(b), ",") { - kv := strings.SplitN(strings.TrimSpace(token), "=", 2) - if len(kv) == 2 { - if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' { - kv[1] = kv[1][1 : len(kv[1])-1] - } - tokens[kv[0]] = kv[1] + + a := strings.SplitN(o.User, "@", 2) + if len(a) != 2 { + return errors.New("xmpp: invalid username (want user@domain): " + o.User) + } + user := a[0] + domain := a[1] + + if m == "PLAIN" { + mechanism = m + // Plain authentication: send base64-encoded \x00 user \x00 password. + raw := "\x00" + user + "\x00" + o.Password + enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) + base64.StdEncoding.Encode(enc, []byte(raw)) + fmt.Fprintf(c.conn, "%s\n", nsSASL, enc) + break + } + if m == "DIGEST-MD5" { + mechanism = m + // Digest-MD5 authentication + fmt.Fprintf(c.conn, "\n", nsSASL) + var ch saslChallenge + if err = c.p.DecodeElement(&ch, nil); err != nil { + return errors.New("unmarshal : " + err.Error()) } - } - realm, _ := tokens["realm"] - nonce, _ := tokens["nonce"] - qop, _ := tokens["qop"] - charset, _ := tokens["charset"] - cnonceStr := cnonce() - digestURI := "xmpp/" + domain - nonceCount := fmt.Sprintf("%08x", 1) - digest := saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", digestURI, nonceCount) - message := "username=\"" + user + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", cnonce=\"" + cnonceStr + - "\", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=\"" + digestURI + "\", response=" + digest + ", charset=" + charset + b, err := base64.StdEncoding.DecodeString(string(ch)) + if err != nil { + return err + } + tokens := map[string]string{} + for _, token := range strings.Split(string(b), ",") { + kv := strings.SplitN(strings.TrimSpace(token), "=", 2) + if len(kv) == 2 { + if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' { + kv[1] = kv[1][1 : len(kv[1])-1] + } + tokens[kv[0]] = kv[1] + } + } + realm, _ := tokens["realm"] + nonce, _ := tokens["nonce"] + qop, _ := tokens["qop"] + charset, _ := tokens["charset"] + cnonceStr := cnonce() + digestURI := "xmpp/" + domain + nonceCount := fmt.Sprintf("%08x", 1) + digest := saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", digestURI, nonceCount) + message := "username=\"" + user + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", cnonce=\"" + cnonceStr + + "\", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=\"" + digestURI + "\", response=" + digest + ", charset=" + charset - fmt.Fprintf(c.conn, "%s\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message))) + fmt.Fprintf(c.conn, "%s\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message))) - var rspauth saslRspAuth - if err = c.p.DecodeElement(&rspauth, nil); err != nil { - return errors.New("unmarshal : " + err.Error()) + var rspauth saslRspAuth + if err = c.p.DecodeElement(&rspauth, nil); err != nil { + return errors.New("unmarshal : " + err.Error()) + } + b, err = base64.StdEncoding.DecodeString(string(rspauth)) + if err != nil { + return err + } + fmt.Fprintf(c.conn, "\n", nsSASL) + break } - b, err = base64.StdEncoding.DecodeString(string(rspauth)) - if err != nil { - return err - } - fmt.Fprintf(c.conn, "\n", nsSASL) - break + } + if mechanism == "" { + return fmt.Errorf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism) } } - if mechanism == "" { - return fmt.Errorf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism) - } - // Next message should be either success or failure. name, val, err := next(c.p) if err != nil { @@ -514,10 +530,19 @@ type Chat struct { Remote string Type string Text string + Roster Roster Other []string Stamp time.Time } +type Roster []Contact + +type Contact struct { + Remote string + Name string + Group []string +} + // Presence is an XMPP presence notification. type Presence struct { From string @@ -541,13 +566,19 @@ func (c *Client) Recv() (stanza interface{}, err error) { v.Delay.Stamp, ) chat := Chat{ - v.From, - v.Type, - v.Body, - v.Other, - stamp, + Remote: v.From, + Type: v.Type, + Text: v.Body, + Other: v.Other, + Stamp: stamp, } return chat, nil + case *clientQuery: + var r Roster + for _, item := range v.Item { + r = append(r, Contact{item.Jid, item.Name, item.Group}) + } + return Chat{Type: "roster", Roster: r}, nil case *clientPresence: return Presence{v.From, v.To, v.Type, v.Show}, nil } @@ -574,6 +605,12 @@ func (c *Client) SendHtml(chat Chat) (n int, err error) { xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), chat.Text) } +// Roster asks for the chat roster. +func (c *Client) Roster() error { + fmt.Fprintf(c.conn, "\n", xmlEscape(c.jid)) + return nil +} + // RFC 3920 C.1 Streams name space type streamFeatures struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` @@ -700,11 +737,23 @@ type clientError struct { Text string } +type clientQuery struct { + Item []rosterItem +} + +type rosterItem struct { + XMLName xml.Name `xml:"jabber:iq:roster item"` + Jid string `xml:",attr"` + Name string `xml:",attr"` + Subscription string `xml:",attr"` + Group []string +} + // Scan XML token stream to find next StartElement. func nextStart(p *xml.Decoder) (xml.StartElement, error) { for { t, err := p.Token() - if err != nil && err != io.EOF { + if err != nil && err != io.EOF || t == nil { return xml.StartElement{}, err } switch t := t.(type) { diff --git a/xmpp_subscription.go b/xmpp_subscription.go new file mode 100644 index 0000000..b714c12 --- /dev/null +++ b/xmpp_subscription.go @@ -0,0 +1,20 @@ +package xmpp + +import ( + "fmt" +) + +func (c* Client) ApproveSubscription(jid string) { + fmt.Fprintf(c.conn, "", + xmlEscape(jid)) +} + +func (c* Client) RevokeSubscription(jid string) { + fmt.Fprintf(c.conn, "", + xmlEscape(jid)) +} + +func (c* Client) RequestSubscription(jid string) { + fmt.Fprintf(c.conn, "", + xmlEscape(jid)) +}