From 6c0bb895d935f5f03d261f8b9785c3658de1b214 Mon Sep 17 00:00:00 2001 From: Sergei Date: Sun, 24 Sep 2023 14:06:38 -0700 Subject: [PATCH] Add config for glangci-lint, lint, handle "wire" errors fo auth procedure --- .golangci.yml | 66 ++++ README.md | 10 +- go.mod | 5 +- go.sum | 2 + xmpp.go | 672 +++++++++++++++++++++++++++++--------- xmpp_avatar.go | 46 +-- xmpp_disco.go | 5 +- xmpp_information_query.go | 16 +- xmpp_muc.go | 41 ++- xmpp_ping.go | 7 +- xmpp_pubsub.go | 10 +- xmpp_subscription.go | 24 +- 12 files changed, 679 insertions(+), 225 deletions(-) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..522d58c --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,66 @@ +run: + tests: false + skip-dirs: + - _example + modules-download-mode: readonly + allow-parallel-runners: true + go: '1.21' +output: + sort-results: true +linters-settings: + gosec: + excludes: + # Оно не используется для повышения безопасности или каких-то таких секъюрых вещей, оно используется для выборки + # произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью. + - G404 # Insecure random number source (rand) +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - asasalint + - asciicheck + - bodyclose + - containedctx + - contextcheck + - decorder + - durationcheck + - errchkjson + - errorlint + - exhaustive + - exportloopref + - gci + - ginkgolinter + - gocheckcompilerdirectives + - gocritic + - godot + - gofmt + - goimports + - gosec + - gosmopolitan + - grouper + - importas + - ireturn + - makezero + - mirror + - nakedret + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - stylecheck + - unconvert + - wastedassign + - whitespace + - wsl diff --git a/README.md b/README.md index 92590da..75224d2 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ go-xmpp go xmpp library. -[Original](https://github.com/rsc/rsc/blob/master/xmpp/xmpp.go) was done by [Russ Cox](https://github.com/rsc), then -forked by [Yasuhiro Matsumoto](https://github.com/mattn). +[Original](https://github.com/rsc/rsc/blob/master/xmpp/xmpp.go) work done by [Russ Cox](https://github.com/rsc), +then [Yasuhiro Matsumoto](https://github.com/mattn) fork it and continue to improve it. -Since both of them have amost zero interest in continuing develop of this lib, i decide to fork it. +Since both of them have almost zero interest to continue develop this lib, I decide to fork it. Documentation can be found at [godoc project](https://godoc.org/github.com/eleksir/go-xmpp) -This fork mainains my needs for xmpp protocol. Feel free to contribute via PR-s if you find it interesing too and feel -that it needs more love than it already has. +This fork maintains my needs for xmpp protocol. Feel free to contribute via PR-s if you find it interesting too and feel +that it needs more love than it already has. And of course fell free to for it if needed. diff --git a/go.mod b/go.mod index dcb498a..d5a123f 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/eleksir/go-xmpp go 1.20 -require golang.org/x/crypto v0.10.0 +require ( + golang.org/x/crypto v0.10.0 + golang.org/x/text v0.10.0 +) diff --git a/go.sum b/go.sum index 778032a..abcf136 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/xmpp.go b/xmpp.go index ae91ad1..29d2e6d 100644 --- a/xmpp.go +++ b/xmpp.go @@ -16,9 +16,9 @@ import ( "bufio" "bytes" "crypto/hmac" - "crypto/md5" + "crypto/md5" //nolint:gosec // go fuck yourself, gosec "crypto/rand" - "crypto/sha1" + "crypto/sha1" //nolint:gosec // go fuck yourself, gosec "crypto/sha256" "crypto/sha512" "crypto/tls" @@ -34,11 +34,13 @@ import ( "net/http" "net/url" "os" + "regexp" "strconv" "strings" "time" "golang.org/x/crypto/pbkdf2" + "golang.org/x/text/encoding/charmap" ) const ( @@ -50,14 +52,14 @@ const ( nsSession = "urn:ietf:params:xml:ns:xmpp-session" ) -// Default TLS configuration options -var DefaultConfig = &tls.Config{} +// DefaultConfig is default TLS configuration options. +var DefaultConfig = &tls.Config{} //nolint:gosec // go fuck yourself, gosec // DebugWriter is the writer used to write debugging output to. var DebugWriter io.Writer = os.Stderr var StanzaWriter io.Writer -// Cookie is a unique XMPP session identifier +// Cookie is a unique XMPP session identifier. type Cookie uint64 func getCookie() Cookie { @@ -65,10 +67,11 @@ func getCookie() Cookie { if _, err := rand.Reader.Read(buf[:]); err != nil { panic("Failed to read random bytes: " + err.Error()) } + return Cookie(binary.LittleEndian.Uint64(buf[:])) } -// Client holds XMPP connection options +// Client holds XMPP connection options. type Client struct { conn net.Conn // connection to server jid string // Jabber ID for our connection @@ -82,6 +85,7 @@ func (c *Client) JID() string { func containsIgnoreCase(s, substr string) bool { s, substr = strings.ToUpper(s), strings.ToUpper(substr) + return strings.Contains(s, substr) } @@ -94,7 +98,9 @@ func connect(host, user, passwd string, timeout time.Duration) (net.Conn, error) addr = a[1] } } + a := strings.SplitN(host, ":", 2) + if len(a) == 1 { addr += ":5222" } @@ -106,53 +112,71 @@ func connect(host, user, passwd string, timeout time.Duration) (net.Conn, error) // test for no proxy, takes a comma separated list with substrings to match if proxy != "" { noproxy := os.Getenv("NO_PROXY") + if noproxy == "" { noproxy = os.Getenv("no_proxy") } + if noproxy != "" { nplist := strings.Split(noproxy, ",") + for _, s := range nplist { if containsIgnoreCase(addr, s) { proxy = "" + break } } } } + if proxy != "" { url, err := url.Parse(proxy) + if err == nil { addr = url.Host } } - c, err := net.DialTimeout("tcp", addr, timeout) + conn, err := net.DialTimeout("tcp", addr, timeout) if err != nil { return nil, err } if proxy != "" { - fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host) - fmt.Fprintf(c, "Host: %s\r\n", host) - fmt.Fprintf(c, "\r\n") - br := bufio.NewReader(c) - req, _ := http.NewRequest("CONNECT", host, nil) - resp, err := http.ReadResponse(br, req) + if _, err := fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\n", host); err != nil { + return nil, err + } + + if _, err := fmt.Fprintf(conn, "Host: %s\r\n", host); err != nil { + return nil, err + } + + if _, err := fmt.Fprintf(conn, "\r\n"); err != nil { + return nil, err + } + + br := bufio.NewReader(conn) + req, _ := http.NewRequest("CONNECT", host, nil) //nolint:noctx // In this case we don't care about context + resp, err := http.ReadResponse(br, req) //nolint:bodyclose // The point is NOT to close proxied connection + if err != nil { return nil, err } - if resp.StatusCode != 200 { + + if resp.StatusCode != http.StatusOK { f := strings.SplitN(resp.Status, " ", 2) return nil, errors.New(f[1]) } } - return c, nil + + return conn, nil } // Options are used to specify additional options for new clients, such as a Resource. type Options struct { // Host specifies what host to connect to, as either "hostname" or "hostname:port" - // If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID. + // If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID. // Default the port to 5222. Host string @@ -220,6 +244,7 @@ func (o Options) NewClient() (*Client, error) { // default to first record host = fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port) defP := addrs[0].Priority + for _, adr := range addrs { if adr.Priority < defP { host = fmt.Sprintf("%s:%d", adr.Target, adr.Port) @@ -234,7 +259,9 @@ func (o Options) NewClient() (*Client, error) { } } } + c, err := connect(host, o.User, o.Password, o.DialTimeout) + if err != nil { return nil, err } @@ -244,6 +271,7 @@ func (o Options) NewClient() (*Client, error) { } client := new(Client) + if o.NoTLS { client.conn = c } else { @@ -272,7 +300,6 @@ func (o Options) NewClient() (*Client, error) { } if err := client.init(&o); err != nil { - client.Close() return nil, err } @@ -290,10 +317,11 @@ func NewClient(host, user, passwd string, debug bool) (*Client, error) { Debug: debug, Session: false, } + return opts.NewClient() } -// NewClientNoTLS creates a new client without TLS +// NewClientNoTLS creates a new client without TLS. func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) { opts := Options{ Host: host, @@ -303,21 +331,24 @@ func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) { Debug: debug, Session: false, } + return opts.NewClient() } -// Close closes the XMPP connection +// Close closes the XMPP connection. func (c *Client) Close() error { if c.conn != (*tls.Conn)(nil) { return c.conn.Close() } + return nil } func saslDigestResponse(username, realm, passwd, nonce, cnonceStr, authenticate, digestURI, nonceCountStr string) string { h := func(text string) []byte { - h := md5.New() + h := md5.New() //nolint:gosec // go fuck yourself, gosec h.Write([]byte(text)) + return h.Sum(nil) } hex := func(bytes []byte) string { @@ -330,6 +361,7 @@ func saslDigestResponse(username, realm, passwd, nonce, cnonceStr, authenticate, a1 := string(h(username+":"+realm+":"+passwd)) + ":" + nonce + ":" + cnonceStr a2 := authenticate + ":" + digestURI response := hex(kd(hex(h(a1)), nonce+":"+nonceCountStr+":"+cnonceStr+":auth:"+hex(h(a2)))) + return response } @@ -337,23 +369,29 @@ func cnonce() string { randSize := big.NewInt(0) randSize.Lsh(big.NewInt(1), 64) cn, err := rand.Int(rand.Reader, randSize) + if err != nil { return "" } + return fmt.Sprintf("%016x", cn) } func (c *Client) init(o *Options) error { + var ( + domain string + user string + ) - var domain string - var user string a := strings.SplitN(o.User, "@", 2) + // Check if User is not empty. Otherwise, we'll be attempting ANONYMOUS with Host domain. switch { case len(o.User) > 0: if len(a) != 2 { return errors.New("xmpp: invalid username (want user@domain): " + o.User) } + user = a[0] domain = a[1] case strings.Contains(o.Host, ":"): @@ -372,19 +410,30 @@ func (c *Client) init(o *Options) error { if f, err = c.startTLSIfRequired(f, o, domain); err != nil { return err } - var mechanism string - var serverSignature []byte + + var ( + mechanism string + serverSignature []byte + ) + if o.User == "" && o.Password == "" { foundAnonymous := false + for _, m := range f.Mechanisms.Mechanism { if m == "ANONYMOUS" { - fmt.Fprintf(StanzaWriter, "\n", nsSASL) + _, err := fmt.Fprintf(StanzaWriter, "\n", nsSASL) + if err != nil { + return err + } + foundAnonymous = true + break } } + if !foundAnonymous { - return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified") + return fmt.Errorf("server does not allow ANONYMOUS authentication and username and password were not specified") } } else { // Even digest forms of authentication are unsafe if we do not know that the host @@ -423,7 +472,21 @@ func (c *Client) init(o *Options) error { } } if strings.HasPrefix(mechanism, "SCRAM-SHA") { + // So here we are. XMPP folks "moved to historical" digest-md5 method + // https://datatracker.ietf.org/doc/html/rfc6331 + // Looks like beacause of security shiteaters squealing about md5 "insecurity". And instead of peeing + // against winds, xmpp.org decide to deprecate digest mechanics and use scram, it is here anyway, so why + // not? Wise decision, kind of. But sooner or later "pseudo-security fanboys" will begin their raid against + // this already-not-so-new scram-thing. In-fact, they already abuse sha1, + // https://datatracker.ietf.org/doc/html/rfc6194 + // Authentication can be considered relatively secure even via plain mechanism only via properly encrypted + // communication, leave this security thing to ssl. Do not try to invent secure auth via unencryped + // communication channels, it is labour of Sisyphus. Anyway, enough of rant. var shaNewFn func() hash.Hash + + // SCRAM-SHA256 vaguely described in https://datatracker.ietf.org/doc/html/rfc7677 which is main document + // for SCRAM-SHA* implementation. So we have to use more detailed description on + // https://wiki.xmpp.org/web/SASL_Authentication_and_SCRAM as reference. switch mechanism { case "SCRAM-SHA-512": shaNewFn = sha512.New @@ -434,22 +497,58 @@ func (c *Client) init(o *Options) error { default: return errors.New("unsupported auth mechanism") } + + // 1. Normalization of utf-8 strings, namely - password, to avoid any variations that utf-8 allows. + // Normalization requirements described there, in common terms https://datatracker.ietf.org/doc/html/rfc4013 + // We miss it here because this task is tedious/difficult to perform completely. So we absolutely trust in + // copy-paste :) + + // 2. Pick a random string clientNonce := cnonce() - clientFirstMessage := "n=" + user + ",r=" + clientNonce - fmt.Fprintf(StanzaWriter, "%s", - nsSASL, mechanism, base64.StdEncoding.EncodeToString([]byte("n,,"+ - clientFirstMessage))) - var sfm string - if err = c.p.DecodeElement(&sfm, nil); err != nil { - return errors.New("unmarshal : " + err.Error()) - } - b, err := base64.StdEncoding.DecodeString(string(sfm)) + + // 3. Prepare initial message. + initialMessage := fmt.Sprintf("n=%s,r=%s", user, clientNonce) + + // 4. The client prepends the GS2 header ("n,,"). Base64 it and send to server. + initialMessageGS2 := []byte("n,," + initialMessage) + b64Buffer := base64.StdEncoding.EncodeToString(initialMessageGS2) + + _, err := fmt.Fprintf( + StanzaWriter, + "%s", + nsSASL, + mechanism, + b64Buffer, + ) + if err != nil { return err } - var serverNonce string - var salt []byte - var iterations int + + // 5. The server responds with a challenge. The data of the challenge is base64 encoded. + var sfm string + + if err = c.p.DecodeElement(&sfm, nil); err != nil { + return errors.New("unmarshal : " + err.Error()) + } + + // 6. The client base64 decodes it. + b, err := base64.StdEncoding.DecodeString(sfm) + if err != nil { + return err + } + + // 7. The client parses it decoded string. + // Here we have + // r as serverNonce + // s as salt + // i as iterations + var ( + serverNonce string + salt []byte + iterations int + ) + for _, serverReply := range strings.Split(string(b), ",") { switch { case strings.HasPrefix(serverReply, "r="): @@ -459,6 +558,11 @@ func (c *Client) init(o *Options) error { } case strings.HasPrefix(serverReply, "s="): salt, err = base64.StdEncoding.DecodeString(strings.SplitN(serverReply, "=", 2)[1]) + + if err != nil { + return err + } + if string(salt) == "" { return errors.New("SCRAM: server sent empty salt") } @@ -472,17 +576,22 @@ func (c *Client) init(o *Options) error { return errors.New("unexpected conted in SCRAM challenge") } } - clientFinalMessageBare := "c=biws,r=" + serverNonce - saltedPassword := pbkdf2.Key([]byte(o.Password), salt, - iterations, shaNewFn().Size(), shaNewFn) - h := hmac.New(shaNewFn, saltedPassword) - _, err = h.Write([]byte("Client Key")) - if err != nil { + + // 8. The client computes parameters for its final message + var ( + clientFinalMessageBare = fmt.Sprintf("c=biws,r=%s", serverNonce) + saltedPassword = pbkdf2.Key([]byte(o.Password), salt, iterations, shaNewFn().Size(), shaNewFn) + h = hmac.New(shaNewFn, saltedPassword) + ) + + if _, err = h.Write([]byte("Client Key")); err != nil { return err } + clientKey := h.Sum(nil) h.Reset() var storedKey []byte + switch mechanism { case "SCRAM-SHA-512": storedKey512 := sha512.Sum512(clientKey) @@ -491,81 +600,140 @@ func (c *Client) init(o *Options) error { storedKey256 := sha256.Sum256(clientKey) storedKey = storedKey256[:] case "SCRAM-SHA-1": - storedKey1 := sha1.Sum(clientKey) + storedKey1 := sha1.Sum(clientKey) //nolint:gosec // fuck yourself, gosec storedKey = storedKey1[:] } - _, err = h.Write([]byte("Server Key")) - if err != nil { + + if _, err = h.Write([]byte("Server Key")); err != nil { return err } + serverFirstMessage, err := base64.StdEncoding.DecodeString(sfm) if err != nil { return err } - authMessage := clientFirstMessage + "," + string(serverFirstMessage) + - "," + clientFinalMessageBare - h = hmac.New(shaNewFn, storedKey[:]) - _, err = h.Write([]byte(authMessage)) - if err != nil { + + authMessage := fmt.Sprintf( + "%s,%s,%s", + initialMessage, + string(serverFirstMessage), + clientFinalMessageBare, + ) + + h = hmac.New(shaNewFn, storedKey) + + if _, err = h.Write([]byte(authMessage)); err != nil { return err } + clientSignature := h.Sum(nil) h.Reset() + if len(clientKey) != len(clientSignature) { return errors.New("SCRAM: client key and signature length mismatch") } + clientProof := make([]byte, len(clientKey)) + for i := range clientKey { clientProof[i] = clientKey[i] ^ clientSignature[i] } + h = hmac.New(shaNewFn, saltedPassword) - _, err = h.Write([]byte("Server Key")) - if err != nil { + + if _, err = h.Write([]byte("Server Key")); err != nil { return err } + serverKey := h.Sum(nil) h.Reset() h = hmac.New(shaNewFn, serverKey) - _, err = h.Write([]byte(authMessage)) - if err != nil { + + if _, err = h.Write([]byte(authMessage)); err != nil { return err } + serverSignature = h.Sum(nil) + if string(serverSignature) == "" { return errors.New("SCRAM: calculated an empty server signature") } - clientFinalMessage := base64.StdEncoding.EncodeToString([]byte(clientFinalMessageBare + - ",p=" + base64.StdEncoding.EncodeToString(clientProof))) - fmt.Fprintf(StanzaWriter, "%s", nsSASL, - clientFinalMessage) - } - if mechanism == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" { - // Oauth authentication: send base64-encoded \x00 user \x00 token. - raw := "\x00" + user + "\x00" + o.OAuthToken - enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) - base64.StdEncoding.Encode(enc, []byte(raw)) - fmt.Fprintf(StanzaWriter, "%s\n", nsSASL, o.OAuthXmlNs, enc) - } - if mechanism == "PLAIN" { - // 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) - } - if mechanism == "DIGEST-MD5" { - // Digest-MD5 authentication - fmt.Fprintf(StanzaWriter, "\n", nsSASL) - var ch saslChallenge - if err = c.p.DecodeElement(&ch, nil); err != nil { - return errors.New("unmarshal : " + err.Error()) - } - b, err := base64.StdEncoding.DecodeString(string(ch)) + + clientFinalMessage := base64.StdEncoding.EncodeToString( + []byte( + clientFinalMessageBare + ",p=" + base64.StdEncoding.EncodeToString(clientProof), + ), + ) + + _, err = fmt.Fprintf( + StanzaWriter, + "%s", + nsSASL, + clientFinalMessage, + ) + if err != nil { return err } + } + + if mechanism == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" { + // Oauth authentication is very similar to PLAIN: we send base64-encoded \x00 user \x00 token. + raw := "\x00" + user + "\x00" + o.OAuthToken + enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) + base64.StdEncoding.Encode(enc, []byte(raw)) + _, err := fmt.Fprintf(StanzaWriter, "%s\n", nsSASL, o.OAuthXmlNs, enc) + if err != nil { + return err + } + } + + if mechanism == "PLAIN" { + // Plain authentication: according to rfc4616, it is sufficient to send base64-encoded + // \x00 user \x00 password. + var ( + raw []byte + enc []byte + ) + + raw = append(raw, []byte("\x00")...) + raw = append(raw, []byte(user)...) + raw = append(raw, []byte("\x00")...) + raw = append(raw, []byte(o.Password)...) + + base64.StdEncoding.Encode(enc, raw) + + _, err := fmt.Fprintf(c.conn, "%s\n", nsSASL, enc) + + if err != nil { + return err + } + } + + if mechanism == "DIGEST-MD5" { + // Digest-MD5 authentication + // Step one, https://datatracker.ietf.org/doc/html/rfc2831#section-2.1.1 + _, err := fmt.Fprintf(StanzaWriter, "\n", nsSASL) + + if err != nil { + return err + } + + var ch saslChallenge + + if err = c.p.DecodeElement(&ch, nil); err != nil { + return errors.New("unmarshal : " + err.Error()) + } + + 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 { @@ -575,38 +743,128 @@ func (c *Client) init(o *Options) error { 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(StanzaWriter, "%s\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message))) + // Step two, https://datatracker.ietf.org/doc/html/rfc2831#section-2.1.2 + var ( + realm = tokens["realm"] + nonce = tokens["nonce"] + qop = tokens["qop"] + charset = tokens["charset"] + cnonceStr = cnonce() + digestURI = "xmpp/" + domain + nonceCount = fmt.Sprintf("%08x", 1) + ) + // 2.1.2 part of rfc2831 says about charset: + // This directive, if present, specifies that the client has use UTF-8 encoding for the username + // and password. If not present, the username and password must be encoded in ISO 8859-1 (of which + // US-ASCII is a subset). The client should send this directive only if the server has indicated it + // supports UTF-8. + // So we have to somehow detect that server can do utf-8. Simplest way to check charset that we get in Step + // one. + var ( + digest string + message []byte + ) + + if match, _ := regexp.MatchString("(UTF-8|UTF8|utf-8|utf8)", charset); match { + digest = saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", + digestURI, nonceCount) + } else { + encoder := charmap.ISO8859_1.NewEncoder() + password, err := encoder.Bytes([]byte(o.Password)) + + if err != nil { + return err + } + + digest = saslDigestResponse(user, realm, string(password), nonce, cnonceStr, "AUTHENTICATE", + digestURI, nonceCount) + } + + if match, _ := regexp.MatchString("(UTF-8|UTF8|utf-8|utf8)", charset); match { + message = append(message, []byte(`username="`)...) + message = append(message, []byte(user)...) + message = append(message, []byte(`", "`)...) + } else { + encoder := charmap.ISO8859_1.NewEncoder() + userISO, err := encoder.Bytes([]byte(user)) + + if err != nil { + return err + } + + message = append(message, []byte(`username="`)...) + message = append(message, userISO...) + message = append(message, []byte(`", "`)...) + } + + message = append(message, []byte(fmt.Sprintf(`realm="%s", `, realm))...) + message = append(message, []byte(fmt.Sprintf(`nonce="%s", `, nonce))...) + message = append(message, []byte(fmt.Sprintf(`cnonce="%s", `, cnonceStr))...) + message = append(message, []byte(fmt.Sprintf(`nc="%s", `, nonceCount))...) + message = append(message, []byte(fmt.Sprintf(`qop="%s", `, qop))...) + message = append(message, []byte(fmt.Sprintf(`digest-uri="%s", `, digestURI))...) + message = append(message, []byte(fmt.Sprintf(`response="%s", `, digest))...) + + if match, _ := regexp.MatchString("^(UTF-8|UTF8|utf-8|utf8)$", charset); match { + message = append(message, []byte(`username="`)...) + message = append(message, []byte(charset)...) + message = append(message, []byte(`", "`)...) + } + + _, err = fmt.Fprintf( + StanzaWriter, + "%s\n", + nsSASL, + base64.StdEncoding.EncodeToString(message), + ) + + if err != nil { + return err + } + + // Step three, server checks if we supplied correct response var rspauth saslRspAuth + + // TODO: more accurate decode responses, taking into account tag names. + // In this case success is something like + // cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== + // while fail is something like this: + // 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(StanzaWriter, "\n", nsSASL) + + // Empty b we should understand it as auth error. + if len(b) == 0 { + return errors.New("auth failure: server answer with empty response, looks like incorrect login or password") + } + + // Step four, answer with empty response + _, err = fmt.Fprintf(StanzaWriter, "\n", nsSASL) + + if err != nil { + return err + } } } + 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 { return err } + switch v := val.(type) { case *saslSuccess: if strings.HasPrefix(mechanism, "SCRAM-SHA") { @@ -614,14 +872,18 @@ func (c *Client) init(o *Options) error { if err != nil { return err } + if !strings.HasPrefix(string(successMsg), "v=") { return errors.New("server sent unexpected content in SCRAM success message") } + serverSignatureReply := strings.SplitN(string(successMsg), "v=", 2)[1] serverSignatureRemote, err := base64.StdEncoding.DecodeString(serverSignatureReply) + if err != nil { return err } + if string(serverSignature) != string(serverSignatureRemote) { return errors.New("SCRAM: server signature mismatch") } @@ -633,6 +895,7 @@ func (c *Client) init(o *Options) error { // which gives a description of what failed if there was no text element errorMessage = v.Any.Local } + return errors.New("auth failure: " + errorMessage) default: return errors.New("expected or , got <" + name.Local + "> in " + name.Space) @@ -640,6 +903,7 @@ func (c *Client) init(o *Options) error { // Now that we're authenticated, we're supposed to start the stream over again. // Declare intent to be a jabber client. + // TODO: check if we can squeeze something useful of f (aka server features) if f, err = c.startStream(o, domain); err != nil { return err } @@ -649,27 +913,43 @@ func (c *Client) init(o *Options) error { // Send IQ message asking to bind to the local user name. if o.Resource == "" { - fmt.Fprintf(StanzaWriter, "\n", cookie, nsBind) + _, err = fmt.Fprintf(StanzaWriter, "\n", cookie, nsBind) + if err != nil { + return err + } } else { - fmt.Fprintf(StanzaWriter, "%s\n", cookie, nsBind, o.Resource) + _, err = fmt.Fprintf(StanzaWriter, "%s\n", cookie, nsBind, o.Resource) + if err != nil { + return err + } } + var iq clientIQ + if err = c.p.DecodeElement(&iq, nil); err != nil { return errors.New("unmarshal : " + err.Error()) } - if &iq.Bind == nil { + + if (iq.Bind == bindBind{}) { return errors.New(" result missing ") } + c.jid = iq.Bind.Jid // our local id c.domain = domain if o.Session { // if server support session, open it - fmt.Fprintf(StanzaWriter, "", xmlEscape(domain), cookie, nsSession) + _, err = fmt.Fprintf(StanzaWriter, "", xmlEscape(domain), cookie, nsSession) + if err != nil { + return err + } } // We're connected and can now receive and send messages. - fmt.Fprintf(StanzaWriter, "%s%s", o.Status, o.StatusMessage) + _, err = fmt.Fprintf(StanzaWriter, "%s%s", o.Status, o.StatusMessage) + if err != nil { + return err + } return nil } @@ -688,11 +968,17 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string // the server requires STARTTLS. case !o.StartTLS: // the user wants STARTTLS and the server supports it. - } + } //nolint:wsl + var err error - fmt.Fprintf(StanzaWriter, "\n") + _, err = fmt.Fprintf(StanzaWriter, "\n") + if err != nil { + return f, err + } + var k tlsProceed + if err = c.p.DecodeElement(&k, nil); err != nil { return f, errors.New("unmarshal : " + err.Error()) } @@ -703,11 +989,13 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string // TODO(scott): we should consider using the server's address or reverse lookup tc.ServerName = domain } + t := tls.Client(c.conn, tc) if err = t.Handshake(); err != nil { return f, errors.New("starttls handshake: " + err.Error()) } + c.conn = t // restart our declaration of XMPP stream intentions. @@ -715,6 +1003,7 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string if err != nil { return f, err } + return tf, nil } @@ -743,6 +1032,7 @@ func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) if err != nil { return nil, err } + if se.Name.Space != nsStream || se.Name.Local != "stream" { return nil, fmt.Errorf("expected but got <%v> in %v", se.Name.Local, se.Name.Space) } @@ -754,6 +1044,7 @@ func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) if err = c.p.DecodeElement(f, nil); err != nil { return f, errors.New("unmarshal : " + err.Error()) } + return f, nil } @@ -761,6 +1052,7 @@ func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) // TLS to connect from the outset, or because it successfully used STARTTLS to promote a TCP connection to TLS. func (c *Client) IsEncrypted() bool { _, ok := c.conn.(*tls.Conn) + return ok } @@ -817,6 +1109,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { if err != nil { return Chat{}, err } + switch v := val.(type) { case *clientMessage: if v.Event.XMLNS == XMPPNS_PUBSUB_EVENT { @@ -845,6 +1138,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { "2006-01-02T15:04:05Z", v.Delay.Stamp, ) + chat := Chat{ Remote: v.From, Type: v.Type, @@ -855,12 +1149,14 @@ func (c *Client) Recv() (stanza interface{}, err error) { OtherElem: 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{ @@ -883,13 +1179,15 @@ func (c *Client) Recv() (stanza interface{}, err error) { if err != nil { return Chat{}, err } + fallthrough case v.Type == "error": switch v.ID { case "sub1": // Pubsub subscription failed var errs []clientPubsubError - err := xml.Unmarshal([]byte(v.Error.InnerXML), &errs) + err := xml.Unmarshal(v.Error.InnerXML, &errs) + if err != nil { return PubsubSubscription{}, err } @@ -917,8 +1215,8 @@ func (c *Client) Recv() (stanza interface{}, err error) { if v.Query.XMLName.Local == "pubsub" { // Subscription or unsubscription was successful var sub clientPubsubSubscription - err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub) - if err != nil { + + if err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub); err != nil { return PubsubSubscription{}, err } @@ -932,8 +1230,8 @@ func (c *Client) Recv() (stanza interface{}, err error) { case "unsub1": if v.Query.XMLName.Local == "pubsub" { var sub clientPubsubSubscription - err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub) - if err != nil { + + if err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub); err != nil { return PubsubUnsubscription{}, err } @@ -943,21 +1241,22 @@ func (c *Client) Recv() (stanza interface{}, err error) { Node: sub.Node, Errors: nil, }, nil - } else { - // Unsubscribing MAY contain a pubsub element. But it does - // not have to - return PubsubUnsubscription{ - SubID: "", - JID: v.From, - Node: "", - Errors: nil, - }, nil } + + // Unsubscribing MAY contain a pubsub element. But it does + // not have to + return PubsubUnsubscription{ + SubID: "", + JID: v.From, + Node: "", + Errors: nil, + }, nil + case "info1": if v.Query.XMLName.Space == XMPPNS_DISCO_ITEMS { var itemsQuery clientDiscoItemsQuery - err := xml.Unmarshal(v.InnerXML, &itemsQuery) - if err != nil { + + if err := xml.Unmarshal(v.InnerXML, &itemsQuery); err != nil { return []DiscoItem{}, err } @@ -966,11 +1265,12 @@ func (c *Client) Recv() (stanza interface{}, err error) { Items: clientDiscoItemsToReturn(itemsQuery.Items), }, nil } + case "info3": if v.Query.XMLName.Space == XMPPNS_DISCO_INFO { var disco clientDiscoQuery - err := xml.Unmarshal(v.InnerXML, &disco) - if err != nil { + + if err := xml.Unmarshal(v.InnerXML, &disco); err != nil { return DiscoResult{}, err } @@ -982,11 +1282,12 @@ func (c *Client) Recv() (stanza interface{}, err error) { Identities: clientIdentitiesToReturn(disco.Identities), }, nil } + case "items1", "items3": if v.Query.XMLName.Local == "pubsub" { var p clientPubsubItems - err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) - if err != nil { + + if err := xml.Unmarshal([]byte(v.Query.InnerXML), &p); err != nil { return PubsubItems{}, err } @@ -1034,8 +1335,10 @@ func (c *Client) Recv() (stanza interface{}, err error) { return handleAvatarMetadata(p.Items[0].Body, v }*/ + default: res, err := xml.Marshal(v.Query) + if err != nil { return Chat{}, err } @@ -1043,8 +1346,10 @@ func (c *Client) Recv() (stanza interface{}, err error) { return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type, Query: res}, nil } + case v.Query.XMLName.Local == "": return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type}, nil + default: res, err := xml.Marshal(v.Query) if err != nil { @@ -1058,62 +1363,80 @@ func (c *Client) Recv() (stanza interface{}, err error) { } } -// Send sends the message wrapped inside an XMPP message stanza body. -func (c *Client) Send(chat Chat) (n int, err error) { +// Send sends the message wrapped inside an XMPP message stanza body. Returns amount bytes sent and error (if any). +func (c *Client) Send(chat Chat) (int, error) { var subtext, thdtext, oobtext string + if chat.Subject != `` { subtext = `` + xmlEscape(chat.Subject) + `` } + if chat.Thread != `` { thdtext = `` + xmlEscape(chat.Thread) + `` } + if chat.Ooburl != `` { oobtext = `` + xmlEscape(chat.Ooburl) + `` + if chat.Oobdesc != `` { oobtext += `` + xmlEscape(chat.Oobdesc) + `` } + oobtext += `` } stanza := "" + subtext + "%s" + oobtext + thdtext + "" - return fmt.Fprintf(StanzaWriter, stanza, + n, err := fmt.Fprintf(StanzaWriter, stanza, xmlEscape(chat.Remote), xmlEscape(chat.Type), cnonce(), xmlEscape(chat.Text)) + + return n, err } // SendOOB sends OOB data wrapped inside an XMPP message stanza, without actual body. -func (c *Client) SendOOB(chat Chat) (n int, err error) { +// Returns amount bytes sent and error (if any). +func (c *Client) SendOOB(chat Chat) (int, error) { var thdtext, oobtext string + if chat.Thread != `` { thdtext = `` + xmlEscape(chat.Thread) + `` } + if chat.Ooburl != `` { oobtext = `` + xmlEscape(chat.Ooburl) + `` + if chat.Oobdesc != `` { oobtext += `` + xmlEscape(chat.Oobdesc) + `` } + oobtext += `` } - return fmt.Fprintf(StanzaWriter, ""+oobtext+thdtext+"", + + n, err := fmt.Fprintf(StanzaWriter, ""+oobtext+thdtext+"", xmlEscape(chat.Remote), xmlEscape(chat.Type), cnonce()) + + return n, err } // SendOrg sends the original text without being wrapped in an XMPP message stanza. -func (c *Client) SendOrg(org string) (n int, err error) { - return fmt.Fprint(StanzaWriter, org) +// Returns amount bytes sent and error (if any). +func (c *Client) SendOrg(org string) (int, error) { + n, err := fmt.Fprint(StanzaWriter, org) + + return n, err } // SendPresence sends Presence wrapped inside XMPP presence stanza. -func (c *Client) SendPresence(presence Presence) (n int, err error) { +func (c *Client) SendPresence(presence Presence) (int, error) { // Forge opening presence tag - var buf string = "" + buf += ">" // TODO: there may be optional tag "priority", but former presence type does not take this into account // so either we must follow std, change type xmpp.Presence and break backward compatibility @@ -1136,39 +1459,48 @@ func (c *Client) SendPresence(presence Presence) (n int, err error) { // away, chat, dnd, xa switch presence.Show { case "away", "chat", "dnd", "xa": - buf = buf + fmt.Sprintf("%s", xmlEscape(presence.Show)) + buf += fmt.Sprintf("%s", xmlEscape(presence.Show)) } } if presence.Status != "" { - buf = buf + fmt.Sprintf("%s", xmlEscape(presence.Status)) + buf += fmt.Sprintf("%s", xmlEscape(presence.Status)) } - buf = buf + "" + buf += "" - return fmt.Fprint(StanzaWriter, buf) + n, err := fmt.Fprint(StanzaWriter, buf) + + return n, err } // SendKeepAlive sends a "whitespace keepalive" as described in chapter 4.6.1 of RFC6120. -func (c *Client) SendKeepAlive() (n int, err error) { - return fmt.Fprintf(c.conn, " ") +// Returns amount bytes sent and error (if any). +func (c *Client) SendKeepAlive() (int, error) { + n, err := fmt.Fprintf(c.conn, " ") + + return n, err } -// SendHtml sends the message as HTML as defined by XEP-0071 -func (c *Client) SendHtml(chat Chat) (n int, err error) { - return fmt.Fprintf(StanzaWriter, ""+ +// SendHtml sends the message as HTML as defined by XEP-0071. +// Returns amount bytes sent and error (if any). +func (c *Client) SendHtml(chat Chat) (int, error) { + n, err := fmt.Fprintf(StanzaWriter, ""+ "%s"+ "%s", xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), chat.Text) + + return n, err } // Roster asks for the chat roster. func (c *Client) Roster() error { - fmt.Fprintf(StanzaWriter, "\n", xmlEscape(c.jid)) - return nil + _, err := fmt.Fprintf(StanzaWriter, "\n", xmlEscape(c.jid)) + + return err } -// RFC 3920 C.1 Streams name space +// RFC 3920 C.1 Streams name space. type streamFeatures struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` StartTLS *tlsStartTLS @@ -1183,7 +1515,7 @@ type streamError struct { Text string } -// RFC 3920 C.3 TLS name space +// RFC 3920 C.3 TLS name space. type tlsStartTLS struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"` Required *string `xml:"required"` @@ -1197,7 +1529,7 @@ type tlsFailure struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"` } -// RFC 3920 C.4 SASL name space +// RFC 3920 C.4 SASL name space. type saslMechanisms struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` Mechanism []string `xml:"mechanism"` @@ -1229,14 +1561,14 @@ type saslFailure struct { Text string `xml:"text"` } -// RFC 3920 C.5 Resource binding name space +// RFC 3920 C.5 Resource binding name space. type bindBind struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` Resource string Jid string `xml:"jid"` } -// RFC 3921 B.1 jabber:client +// RFC 3921 B.1 jabber:client. type clientMessage struct { XMLName xml.Name `xml:"jabber:client message"` From string `xml:"from,attr"` @@ -1263,6 +1595,7 @@ func (m *clientMessage) OtherStrings() []string { for i, e := range m.Other { a[i] = e.String() } + return a } @@ -1273,24 +1606,31 @@ type XMLElement struct { } func (e *XMLElement) String() string { - r := bytes.NewReader([]byte(e.InnerXML)) - d := xml.NewDecoder(r) - var buf bytes.Buffer + var ( + r = bytes.NewReader([]byte(e.InnerXML)) + d = xml.NewDecoder(r) + buf bytes.Buffer + ) + for { tok, err := d.Token() + if err != nil { break } + switch v := tok.(type) { case xml.StartElement: err = d.Skip() case xml.CharData: _, err = buf.Write(v) } + if err != nil { break } } + return buf.String() } @@ -1368,7 +1708,8 @@ func nextStart(p *xml.Decoder) (xml.StartElement, error) { if err != nil || t == nil { return xml.StartElement{}, err } - switch t := t.(type) { + + switch t := t.(type) { //nolint:gocritic case xml.StartElement: return t, nil } @@ -1387,6 +1728,7 @@ func next(p *xml.Decoder) (xml.Name, interface{}, error) { // Put it in an interface and allocate one. var nv interface{} + switch se.Name.Space + " " + se.Name.Local { case nsStream + " features": nv = &streamFeatures{} @@ -1435,6 +1777,7 @@ func next(p *xml.Decoder) (xml.Name, interface{}, error) { func xmlEscape(s string) string { var b bytes.Buffer + xml.Escape(&b, []byte(s)) return b.String() @@ -1445,11 +1788,18 @@ type tee struct { w io.Writer } -func (t tee) Read(p []byte) (n int, err error) { - n, err = t.r.Read(p) +func (t tee) Read(p []byte) (int, error) { + n, err := t.r.Read(p) + if n > 0 { - t.w.Write(p[0:n]) - t.w.Write([]byte("\n")) + if m, err := t.w.Write(p[0:n]); err != nil { + return m, err + } + + if m, err := t.w.Write([]byte("\n")); err != nil { + return m, err + } } - return + + return n, err } diff --git a/xmpp_avatar.go b/xmpp_avatar.go index 7303dba..4423dfa 100644 --- a/xmpp_avatar.go +++ b/xmpp_avatar.go @@ -1,7 +1,7 @@ package xmpp import ( - "crypto/sha1" + "crypto/sha1" //nolint:gosec // go fuck yourself, gosec. You don't even fucking read rfc. So shout the fuck up. "encoding/base64" "encoding/hex" "encoding/xml" @@ -10,8 +10,8 @@ import ( ) const ( - XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data" - XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata" + XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data" //nocritic:revive + XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata" //nocritic:revive ) type clientAvatarData struct { @@ -52,8 +52,8 @@ type AvatarMetadata struct { func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) { var data clientAvatarData - err := xml.Unmarshal(itemsBody, &data) - if err != nil { + + if err := xml.Unmarshal(itemsBody, &data); err != nil { return AvatarData{}, err } @@ -64,8 +64,9 @@ func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) { return AvatarData{}, err } - hash := sha1.Sum(dataRaw) + hash := sha1.Sum(dataRaw) //nolint:gosec // go fuck yourself, gosec. This exact mechanics described in standard. hashStr := hex.EncodeToString(hash[:]) + if hashStr != id { return AvatarData{}, errors.New("SHA1 hashes do not match") } @@ -77,9 +78,12 @@ func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) { } func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) { - var meta clientAvatarMetadata - err := xml.Unmarshal(body, &meta) - if err != nil { + var ( + meta clientAvatarMetadata + err error + ) + + if err = xml.Unmarshal(body, &meta); err != nil { return AvatarMetadata{}, err } @@ -91,10 +95,10 @@ func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) { ID: meta.Info.ID, Type: meta.Info.Type, URL: meta.Info.URL, - }, nil + }, err } -// A wrapper for atoi which just returns -1 if an error occurs +// atoiw is a wrapper for atoi which just returns -1 if an error occurs. func atoiw(str string) int { i, err := strconv.Atoi(str) if err != nil { @@ -104,22 +108,22 @@ func atoiw(str string) int { return i } -func (c *Client) AvatarSubscribeMetadata(jid string) { - c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid) +func (c *Client) AvatarSubscribeMetadata(jid string) (string, error) { + return c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid) } -func (c *Client) AvatarUnsubscribeMetadata(jid string) { - c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid) +func (c *Client) AvatarUnsubscribeMetadata(jid string) (string, error) { + return c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid) } -func (c *Client) AvatarRequestData(jid string) { - c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid) +func (c *Client) AvatarRequestData(jid string) (string, error) { + return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid) } -func (c *Client) AvatarRequestDataByID(jid, id string) { - c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id) +func (c *Client) AvatarRequestDataByID(jid, id string) (string, error) { + return c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id) } -func (c *Client) AvatarRequestMetadata(jid string) { - c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid) +func (c *Client) AvatarRequestMetadata(jid string) (string, error) { + return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid) } diff --git a/xmpp_disco.go b/xmpp_disco.go index 4af0426..65f0888 100644 --- a/xmpp_disco.go +++ b/xmpp_disco.go @@ -5,8 +5,8 @@ import ( ) const ( - XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items" - XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info" + XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items" //nocritic:revive + XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info" //nocritic:revive ) type clientDiscoFeature struct { @@ -90,6 +90,7 @@ func clientIdentitiesToReturn(identities []clientDiscoIdentity) []DiscoIdentity func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem { var ret []DiscoItem + for _, item := range items { ret = append(ret, DiscoItem{ Jid: item.Jid, diff --git a/xmpp_information_query.go b/xmpp_information_query.go index 7503283..a3f25e6 100644 --- a/xmpp_information_query.go +++ b/xmpp_information_query.go @@ -14,22 +14,25 @@ const IQTypeError = "error" func (c *Client) Discovery() (string, error) { // use getCookie for a pseudo random id. reqID := strconv.FormatUint(uint64(getCookie()), 10) + return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "") } // DiscoverNodeInfo discovers information about a node. Empty node queries info about server itself. func (c *Client) DiscoverNodeInfo(node string) (string, error) { query := fmt.Sprintf("", XMPPNS_DISCO_INFO, node) + return c.RawInformation(c.jid, c.domain, "info3", IQTypeGet, query) } // DiscoverInfo discovers information about given item from given jid. func (c *Client) DiscoverInfo(from string, to string) (string, error) { query := fmt.Sprintf("", XMPPNS_DISCO_INFO) + return c.RawInformation(from, to, "info3", IQTypeGet, query) } -// DiscoverServerItems discover items that the server exposes +// DiscoverServerItems discover items that the server exposes. func (c *Client) DiscoverServerItems() (string, error) { return c.DiscoverEntityItems(c.domain) } @@ -37,6 +40,7 @@ func (c *Client) DiscoverServerItems() (string, error) { // DiscoverEntityItems discovers items that an entity exposes. func (c *Client) DiscoverEntityItems(jid string) (string, error) { query := fmt.Sprintf("", XMPPNS_DISCO_ITEMS) + return c.RawInformation(c.jid, jid, "info1", IQTypeGet, query) } @@ -44,6 +48,7 @@ func (c *Client) DiscoverEntityItems(jid string) (string, error) { func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) { const xmlIQ = "%s" _, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body) + return id, err } @@ -51,6 +56,7 @@ func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, bod func (c *Client) RawInformation(from, to, id, iqType, body string) (string, error) { const xmlIQ = "%s" _, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body) + return id, err } @@ -64,7 +70,7 @@ func (c *Client) IqVersionResponse(v IQ, name, version, os string) (string, erro version = "undefined" } - query := "" //nolint:wsl + query := "" query += fmt.Sprintf("%s", name) query += fmt.Sprintf("%s", version) @@ -100,9 +106,9 @@ func (c *Client) JabberIqLastResponse(v IQ, lastActivity int64) (string, error) ) } -// UrnXmppTimeResponse implements response to query entity's current time (xep-0202). -// TimezoneOffset have format +HH:MM or -HH:MM -func (c *Client) UrnXmppTimeResponse(v IQ, timezoneOffset string) (string, error) { +// UrnXMPPTimeResponse implements response to query entity's current time (xep-0202). +// TimezoneOffset have format +HH:MM or -HH:MM. +func (c *Client) UrnXMPPTimeResponse(v IQ, timezoneOffset string) (string, error) { query := fmt.Sprintf( "", timezoneOffset, diff --git a/xmpp_muc.go b/xmpp_muc.go index 64798c2..fd3495e 100644 --- a/xmpp_muc.go +++ b/xmpp_muc.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. // TODO(flo): -// - support password protected MUC rooms // - cleanup signatures of join/leave functions package xmpp @@ -23,16 +22,18 @@ const ( SinceHistory = 4 ) -// Send sends room topic wrapped inside an XMPP message stanza body. -func (c *Client) SendTopic(chat Chat) (n int, err error) { +// SendTopic sends room topic wrapped inside an XMPP message stanza body. +func (c *Client) SendTopic(chat Chat) (int, error) { return fmt.Fprintf(StanzaWriter, ""+"%s", xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text)) } -func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) { +// JoinMUCNoHistory joins room and instructs server that no history data required. +func (c *Client) JoinMUCNoHistory(jid, nick string) (int, error) { if nick == "" { nick = c.jid } + return fmt.Fprintf(StanzaWriter, "\n"+ ""+ "\n"+ @@ -40,12 +41,13 @@ func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) { xmlEscape(jid), xmlEscape(nick), nsMUC) } -// xep-0045 7.2 -func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) { +// JoinMUC joins room, xep-0045 7.2. +func (c *Client) JoinMUC(jid, nick string, historyType, history int, historyDate *time.Time) (int, error) { if nick == "" { nick = c.jid } - switch history_type { + + switch historyType { case NoHistory: return fmt.Fprintf(StanzaWriter, "\n"+ "\n"+ @@ -70,23 +72,25 @@ func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_da "", xmlEscape(jid), xmlEscape(nick), nsMUC, history) case SinceHistory: - if history_date != nil { + if historyDate != nil { return fmt.Fprintf(StanzaWriter, "\n"+ "\n"+ "\n"+ "", - xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339)) + xmlEscape(jid), xmlEscape(nick), nsMUC, historyDate.Format(time.RFC3339)) } } - return 0, errors.New("Unknown history option") + + return 0, errors.New("unknown history option") } -// xep-0045 7.2.6 -func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) { +// JoinProtectedMUC joins password protected room, xep-0045 7.2.6. +func (c *Client) JoinProtectedMUC(jid, nick string, password string, historyType, history int, historyDate *time.Time) (int, error) { if nick == "" { nick = c.jid } - switch history_type { + + switch historyType { case NoHistory: return fmt.Fprintf(StanzaWriter, "\n"+ "\n"+ @@ -116,20 +120,21 @@ func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_typ "", xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history) case SinceHistory: - if history_date != nil { + if historyDate != nil { return fmt.Fprintf(StanzaWriter, "\n"+ "\n"+ "%s\n"+ "\n"+ "", - xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339)) + xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), historyDate.Format(time.RFC3339)) } } - return 0, errors.New("Unknown history option") + + return 0, errors.New("unknown history option") } -// xep-0045 7.14 -func (c *Client) LeaveMUC(jid string) (n int, err error) { +// LeaveMUC quits from room xep-0045 7.14. +func (c *Client) LeaveMUC(jid string) (int, error) { return fmt.Fprintf(StanzaWriter, "", c.jid, xmlEscape(jid)) } diff --git a/xmpp_ping.go b/xmpp_ping.go index 0326b9a..77b0aa0 100644 --- a/xmpp_ping.go +++ b/xmpp_ping.go @@ -8,13 +8,16 @@ func (c *Client) PingC2S(jid, server string) error { if jid == "" { jid = c.jid } + if server == "" { server = c.domain } + _, err := fmt.Fprintf(StanzaWriter, "\n"+ "\n"+ "", xmlEscape(jid), xmlEscape(server)) + return err } @@ -23,16 +26,18 @@ func (c *Client) PingS2S(fromServer, toServer string) error { "\n"+ "", xmlEscape(fromServer), xmlEscape(toServer)) + return err } func (c *Client) SendResultPing(id, toServer string) error { _, err := fmt.Fprintf(StanzaWriter, "", xmlEscape(toServer), xmlEscape(id)) + return err } -// PingResponse responding to ping query according to xep-0199 +// PingResponse responding to ping query according to xep-0199. func (c *Client) PingResponse(v IQ) (string, error) { return c.RawInformation( v.To, diff --git a/xmpp_pubsub.go b/xmpp_pubsub.go index 86bb635..b91678e 100644 --- a/xmpp_pubsub.go +++ b/xmpp_pubsub.go @@ -6,8 +6,8 @@ import ( ) const ( - XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub" - XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event" + XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub" //nolint:revive + XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event" //nolint:revive ) type clientPubsubItem struct { @@ -67,7 +67,7 @@ type PubsubItems struct { Items []PubsubItem } -// Converts []clientPubsubItem to []PubsubItem +// Converts []clientPubsubItem to []PubsubItem. func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem { var tmp []PubsubItem for _, i := range items { @@ -96,6 +96,7 @@ func pubsubSubscriptionStanza(node, jid string) string { body := fmt.Sprintf("", xmlEscape(node), xmlEscape(jid)) + return pubsubStanza(body) } @@ -103,6 +104,7 @@ func pubsubUnsubscriptionStanza(node, jid string) string { body := fmt.Sprintf("", xmlEscape(node), xmlEscape(jid)) + return pubsubStanza(body) } @@ -124,10 +126,12 @@ func (c *Client) PubsubUnsubscribeNode(node, jid string) (string, error) { func (c *Client) PubsubRequestLastItems(node, jid string) (string, error) { body := fmt.Sprintf("", node) + return c.RawInformation(c.jid, jid, "items1", "get", pubsubStanza(body)) } func (c *Client) PubsubRequestItem(node, jid, id string) (string, error) { body := fmt.Sprintf("", node, id) + return c.RawInformation(c.jid, jid, "items3", "get", pubsubStanza(body)) } diff --git a/xmpp_subscription.go b/xmpp_subscription.go index ed8289f..5a8e048 100644 --- a/xmpp_subscription.go +++ b/xmpp_subscription.go @@ -4,22 +4,30 @@ import ( "fmt" ) -func (c *Client) ApproveSubscription(jid string) { - fmt.Fprintf(StanzaWriter, "", +func (c *Client) ApproveSubscription(jid string) error { + _, err := fmt.Fprintf(StanzaWriter, "", xmlEscape(jid)) + + return err } -func (c *Client) RevokeSubscription(jid string) { - fmt.Fprintf(StanzaWriter, "", +func (c *Client) RevokeSubscription(jid string) error { + _, err := fmt.Fprintf(StanzaWriter, "", xmlEscape(jid)) + + return err } -func (c *Client) RetrieveSubscription(jid string) { - fmt.Fprintf(c.conn, "", +func (c *Client) RetrieveSubscription(jid string) error { + _, err := fmt.Fprintf(c.conn, "", xmlEscape(jid)) + + return err } -func (c *Client) RequestSubscription(jid string) { - fmt.Fprintf(StanzaWriter, "", +func (c *Client) RequestSubscription(jid string) error { + _, err := fmt.Fprintf(StanzaWriter, "", xmlEscape(jid)) + + return err }