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))
+}