forked from jshiffer/go-xmpp
Fetch latest changes
This commit is contained in:
commit
02e423485e
@ -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 {
|
||||
|
217
xmpp.go
217
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, "<auth xmlns='%s' mechanism='ANONYMOUS' />\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, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n", nsSASL, enc)
|
||||
break
|
||||
}
|
||||
if m == "DIGEST-MD5" {
|
||||
mechanism = m
|
||||
// Digest-MD5 authentication
|
||||
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='DIGEST-MD5'/>\n", nsSASL)
|
||||
var ch saslChallenge
|
||||
if err = c.p.DecodeElement(&ch, nil); err != nil {
|
||||
return errors.New("unmarshal <challenge>: " + err.Error())
|
||||
if o.User == "" && o.Password == "" {
|
||||
foundAnonymous := false
|
||||
for _, m := range f.Mechanisms.Mechanism {
|
||||
if m == "ANONYMOUS" {
|
||||
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='ANONYMOUS' />\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, "<auth xmlns='%s' mechanism='ANONYMOUS' />\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, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n", nsSASL, enc)
|
||||
break
|
||||
}
|
||||
if m == "DIGEST-MD5" {
|
||||
mechanism = m
|
||||
// Digest-MD5 authentication
|
||||
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='DIGEST-MD5'/>\n", nsSASL)
|
||||
var ch saslChallenge
|
||||
if err = c.p.DecodeElement(&ch, nil); err != nil {
|
||||
return errors.New("unmarshal <challenge>: " + 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, "<response xmlns='%s'>%s</response>\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message)))
|
||||
fmt.Fprintf(c.conn, "<response xmlns='%s'>%s</response>\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message)))
|
||||
|
||||
var rspauth saslRspAuth
|
||||
if err = c.p.DecodeElement(&rspauth, nil); err != nil {
|
||||
return errors.New("unmarshal <challenge>: " + err.Error())
|
||||
var rspauth saslRspAuth
|
||||
if err = c.p.DecodeElement(&rspauth, nil); err != nil {
|
||||
return errors.New("unmarshal <challenge>: " + err.Error())
|
||||
}
|
||||
b, err = base64.StdEncoding.DecodeString(string(rspauth))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.conn, "<response xmlns='%s'/>\n", nsSASL)
|
||||
break
|
||||
}
|
||||
b, err = base64.StdEncoding.DecodeString(string(rspauth))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.conn, "<response xmlns='%s'/>\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, "<iq from='%s' type='get' id='roster1'><query xmlns='jabber:iq:roster'/></iq>\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) {
|
||||
|
20
xmpp_subscription.go
Normal file
20
xmpp_subscription.go
Normal file
@ -0,0 +1,20 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (c* Client) ApproveSubscription(jid string) {
|
||||
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribed'/>",
|
||||
xmlEscape(jid))
|
||||
}
|
||||
|
||||
func (c* Client) RevokeSubscription(jid string) {
|
||||
fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribed'/>",
|
||||
xmlEscape(jid))
|
||||
}
|
||||
|
||||
func (c* Client) RequestSubscription(jid string) {
|
||||
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribe'/>",
|
||||
xmlEscape(jid))
|
||||
}
|
Loading…
Reference in New Issue
Block a user