├── CONTRIBUTING ├── LICENSE ├── README.md ├── cmd └── gcm-logger │ └── gcm-logger.go ├── gcm.go └── gcm_test.go /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) 6 | (CLA), which you can do online. The CLA is necessary mainly because you own the 7 | copyright to your changes, even after your contribution becomes part of our 8 | codebase, so we need your permission to use and distribute your code. We also 9 | need to be sure of various other things—for instance that you'll tell us if you 10 | know that your code infringes on other people's patents. You don't have to sign 11 | the CLA until after you've submitted your code for review and a member has 12 | approved it, but you must do it before we can put your code into our codebase. 13 | Before you start working on a larger contribution, you should get in touch with 14 | us first through the issue tracker with your idea so that we can help out and 15 | possibly guide you. Coordinating up front makes it much easier to avoid 16 | frustration later on. 17 | 18 | ### Code reviews 19 | All submissions, including submissions by project members, require review. We 20 | use Github pull requests for this purpose. 21 | 22 | ### The small print 23 | Contributions made by corporations are covered by a different agreement than 24 | the one above, the Software Grant and Corporate Contributor License Agreement. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project status # 2 | ![status: inactive](https://img.shields.io/badge/status-inactive-red.svg) 3 | 4 | This project is no longer actively maintained, and remains here as an archive of this work. 5 | 6 | For a replacement, check out [this actively maintained fork](https://github.com/kikinteractive/go-gcm) of the library. 7 | 8 | GCM Library for Go 9 | -- 10 | 11 | Provides the following functionality for Google Cloud Messaging: 12 | 13 | 1. Sending messages. 14 | 2. Listening to receiving messages. 15 | 16 | Documentation: http://godoc.org/github.com/google/go-gcm 17 | 18 | ## Installation 19 | 20 | $ go get github.com/google/go-gcm 21 | 22 | ## Status 23 | 24 | This library is in Alpha. We will make an effort to support the library, but we reserve the right to make incompatible changes when necessary. 25 | 26 | ## Feedback 27 | 28 | Please read CONTRIBUTING and raise issues here in Github. 29 | -------------------------------------------------------------------------------- /cmd/gcm-logger/gcm-logger.go: -------------------------------------------------------------------------------- 1 | // Program gcm-logger logs and echoes as a GCM "server". 2 | package main 3 | 4 | import ( 5 | "github.com/alecthomas/kingpin" 6 | "github.com/aliafshar/toylog" 7 | "github.com/google/go-gcm" 8 | ) 9 | 10 | var ( 11 | serverKey = kingpin.Flag("server_key", "The server key to use for GCM.").Short('k').Required().String() 12 | senderId = kingpin.Flag("sender_id", "The sender ID to use for GCM.").Short('s').Required().String() 13 | ) 14 | 15 | // onMessage receives messages, logs them, and echoes a response. 16 | func onMessage(cm gcm.CcsMessage) error { 17 | toylog.Infoln("Message, from:", cm.From, "with:", cm.Data) 18 | // Echo the message with a tag. 19 | cm.Data["echoed"] = true 20 | m := gcm.HttpMessage{To: cm.From, Data: cm.Data} 21 | r, err := gcm.SendHttp(*serverKey, m) 22 | if err != nil { 23 | toylog.Errorln("Error sending message.", err) 24 | return err 25 | } 26 | toylog.Infof("Sent message. %+v -> %+v", m, r) 27 | return nil 28 | } 29 | 30 | func main() { 31 | toylog.Infoln("GCM Logger, starting.") 32 | kingpin.Parse() 33 | gcm.Listen(*senderId, *serverKey, onMessage, nil) 34 | } 35 | -------------------------------------------------------------------------------- /gcm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package gcm provides send and receive GCM functionality. 16 | package gcm 17 | 18 | import ( 19 | "bytes" 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | "log" 24 | "net/http" 25 | "strings" 26 | "sync" 27 | "time" 28 | 29 | "github.com/jpillora/backoff" 30 | "github.com/mattn/go-xmpp" 31 | "github.com/pborman/uuid" 32 | ) 33 | 34 | const ( 35 | CCSAck = "ack" 36 | CCSNack = "nack" 37 | CCSControl = "control" 38 | CCSReceipt = "receipt" 39 | httpAddress = "https://gcm-http.googleapis.com/gcm/send" 40 | xmppHost = "gcm.googleapis.com" 41 | xmppPort = "5235" 42 | xmppAddress = xmppHost + ":" + xmppPort 43 | // For ccs the min for exponential backoff has to be 1 sec 44 | ccsMinBackoff = 1 * time.Second 45 | ) 46 | 47 | var ( 48 | // DebugMode determines whether to have verbose logging. 49 | DebugMode = false 50 | // Default Min and Max delay for backoff. 51 | DefaultMinBackoff = 1 * time.Second 52 | DefaultMaxBackoff = 10 * time.Second 53 | retryableErrors = map[string]bool{ 54 | "Unavailable": true, 55 | "SERVICE_UNAVAILABLE": true, 56 | "InternalServerError": true, 57 | "INTERNAL_SERVER_ ERROR": true, 58 | // TODO(silvano): should we backoff with the same strategy on 59 | // DeviceMessageRateExceeded and TopicsMessageRateExceeded. 60 | } 61 | // Cache of xmpp clients. 62 | xmppClients = struct { 63 | sync.Mutex 64 | m map[string]*xmppGcmClient 65 | }{ 66 | m: make(map[string]*xmppGcmClient), 67 | } 68 | ) 69 | 70 | // Prints debug info if DebugMode is set. 71 | func debug(m string, v interface{}) { 72 | if DebugMode { 73 | log.Printf(m+":%+v", v) 74 | } 75 | } 76 | 77 | // A GCM Http message. 78 | type HttpMessage struct { 79 | To string `json:"to,omitempty"` 80 | RegistrationIds []string `json:"registration_ids,omitempty"` 81 | CollapseKey string `json:"collapse_key,omitempty"` 82 | Priority string `json:"priority,omitempty"` 83 | ContentAvailable bool `json:"content_available,omitempty"` 84 | DelayWhileIdle bool `json:"delay_while_idle,omitempty"` 85 | TimeToLive *uint `json:"time_to_live,omitempty"` 86 | RestrictedPackageName string `json:"restricted_package_name,omitempty"` 87 | DryRun bool `json:"dry_run,omitempty"` 88 | Data Data `json:"data,omitempty"` 89 | Notification *Notification `json:"notification,omitempty"` 90 | } 91 | 92 | // A GCM Xmpp message. 93 | type XmppMessage struct { 94 | To string `json:"to,omitempty"` 95 | MessageId string `json:"message_id"` 96 | MessageType string `json:"message_type,omitempty"` 97 | CollapseKey string `json:"collapse_key,omitempty"` 98 | Priority string `json:"priority,omitempty"` 99 | ContentAvailable bool `json:"content_available,omitempty"` 100 | DelayWhileIdle bool `json:"delay_while_idle,omitempty"` 101 | TimeToLive *uint `json:"time_to_live,omitempty"` 102 | DeliveryReceiptRequested bool `json:"delivery_receipt_requested,omitempty"` 103 | DryRun bool `json:"dry_run,omitempty"` 104 | Data Data `json:"data,omitempty"` 105 | Notification *Notification `json:"notification,omitempty"` 106 | } 107 | 108 | // HttpResponse is the GCM connection server response to an HTTP downstream message request. 109 | type HttpResponse struct { 110 | MulticastId int `json:"multicast_id,omitempty"` 111 | Success uint `json:"success,omitempty"` 112 | Failure uint `json:"failure,omitempty"` 113 | CanonicalIds uint `json:"canonical_ids,omitempty"` 114 | Results []Result `json:"results,omitempty"` 115 | MessageId int `json:"message_id,omitempty"` 116 | Error string `json:"error,omitempty"` 117 | } 118 | 119 | // Result represents the status of a processed Http message. 120 | type Result struct { 121 | MessageId string `json:"message_id,omitempty"` 122 | RegistrationId string `json:"registration_id,omitempty"` 123 | Error string `json:"error,omitempty"` 124 | } 125 | 126 | // CcsMessage is an Xmpp message sent from CCS. 127 | type CcsMessage struct { 128 | From string `json:"from, omitempty"` 129 | MessageId string `json:"message_id, omitempty"` 130 | MessageType string `json:"message_type, omitempty"` 131 | RegistrationId string `json:"registration_id,omitempty"` 132 | Error string `json:"error,omitempty"` 133 | ErrorDescription string `json:"error_description,omitempty"` 134 | Category string `json:"category, omitempty"` 135 | Data Data `json:"data,omitempty"` 136 | ControlType string `json:"control_type,omitempty"` 137 | } 138 | 139 | // Used to compute results for multicast messages with retries. 140 | type multicastResultsState map[string]*Result 141 | 142 | // The data payload of a GCM message. 143 | type Data map[string]interface{} 144 | 145 | // The notification payload of a GCM message. 146 | type Notification struct { 147 | Title string `json:"title,omitempty"` 148 | Body string `json:"body,omitempty"` 149 | Icon string `json:"icon,omitempty"` 150 | Sound string `json:"sound,omitempty"` 151 | Badge string `json:"badge,omitempty"` 152 | Tag string `json:"tag,omitempty"` 153 | Color string `json:"color,omitempty"` 154 | ClickAction string `json:"click_action,omitempty"` 155 | BodyLocKey string `json:"body_loc_key,omitempty"` 156 | BodyLocArgs string `json:"body_loc_args,omitempty"` 157 | TitleLocArgs string `json:"title_loc_args,omitempty"` 158 | TitleLocKey string `json:"title_loc_key,omitempty"` 159 | } 160 | 161 | // MessageHandler is the type for a function that handles a CCS message. 162 | // The CCS message can be an upstream message (device to server) or a 163 | // message from CCS (e.g. a delivery receipt). 164 | type MessageHandler func(cm CcsMessage) error 165 | 166 | // httpClient is an interface to stub the http client in tests. 167 | type httpClient interface { 168 | send(apiKey string, m HttpMessage) (*HttpResponse, error) 169 | getRetryAfter() string 170 | } 171 | 172 | // httpGcmClient is a client for the Gcm Http Connection Server. 173 | type httpGcmClient struct { 174 | GcmURL string 175 | HttpClient *http.Client 176 | retryAfter string 177 | } 178 | 179 | // httpGcmClient implementation to send a message through GCM Http server. 180 | func (c *httpGcmClient) send(apiKey string, m HttpMessage) (*HttpResponse, error) { 181 | bs, err := json.Marshal(m) 182 | if err != nil { 183 | return nil, fmt.Errorf("error marshalling message>%v", err) 184 | } 185 | debug("sending", string(bs)) 186 | req, err := http.NewRequest("POST", c.GcmURL, bytes.NewReader(bs)) 187 | if err != nil { 188 | return nil, fmt.Errorf("error creating request>%v", err) 189 | } 190 | req.Header.Add(http.CanonicalHeaderKey("Content-Type"), "application/json") 191 | req.Header.Add(http.CanonicalHeaderKey("Authorization"), authHeader(apiKey)) 192 | httpResp, err := c.HttpClient.Do(req) 193 | if err != nil { 194 | return nil, fmt.Errorf("error sending request to HTTP connection server>%v", err) 195 | } 196 | gcmResp := &HttpResponse{} 197 | body, err := ioutil.ReadAll(httpResp.Body) 198 | defer httpResp.Body.Close() 199 | if err != nil { 200 | return gcmResp, fmt.Errorf("error reading http response body>%v", err) 201 | } 202 | debug("received body", string(body)) 203 | err = json.Unmarshal(body, &gcmResp) 204 | if err != nil { 205 | return gcmResp, fmt.Errorf("error unmarshaling json from body: %v", err) 206 | } 207 | // TODO(silvano): this is assuming that the header contains seconds instead of a date, need to check 208 | c.retryAfter = httpResp.Header.Get(http.CanonicalHeaderKey("Retry-After")) 209 | return gcmResp, nil 210 | } 211 | 212 | // Get the value of the retry after header if present. 213 | func (c httpGcmClient) getRetryAfter() string { 214 | return c.retryAfter 215 | } 216 | 217 | // xmppClient is an interface to stub the xmpp client in tests. 218 | type xmppClient interface { 219 | listen(h MessageHandler, stop chan<- bool) error 220 | send(m XmppMessage) (int, error) 221 | } 222 | 223 | // xmppGcmClient is a client for the Gcm Xmpp Connection Server (CCS). 224 | type xmppGcmClient struct { 225 | sync.RWMutex 226 | XmppClient xmpp.Client 227 | messages struct { 228 | sync.RWMutex 229 | m map[string]*messageLogEntry 230 | } 231 | senderID string 232 | isClosed bool 233 | } 234 | 235 | // An entry in the messages log, used to keep track of messages pending ack and 236 | // retries for failed messages. 237 | type messageLogEntry struct { 238 | body *XmppMessage 239 | backoff *exponentialBackoff 240 | } 241 | 242 | // Factory method for xmppGcmClient, to minimize the number of clients to one per sender id. 243 | // TODO(silvano): this could be revised, taking into account that we cannot have more than 1000 244 | // connections per senderId. 245 | func newXmppGcmClient(senderID string, apiKey string) (*xmppGcmClient, error) { 246 | xmppClients.Lock() 247 | defer xmppClients.Unlock() 248 | if xc, ok := xmppClients.m[senderID]; ok { 249 | return xc, nil 250 | } 251 | 252 | nc, err := xmpp.NewClient(xmppAddress, xmppUser(senderID), apiKey, DebugMode) 253 | if err != nil { 254 | return nil, fmt.Errorf("error connecting client>%v", err) 255 | } 256 | 257 | xc := &xmppGcmClient{ 258 | XmppClient: *nc, 259 | messages: struct { 260 | sync.RWMutex 261 | m map[string]*messageLogEntry 262 | }{ 263 | m: make(map[string]*messageLogEntry), 264 | }, 265 | senderID: senderID, 266 | } 267 | 268 | xmppClients.m[senderID] = xc 269 | return xc, nil 270 | } 271 | 272 | // xmppGcmClient implementation of listening for messages from CCS; the messages can be 273 | // acks or nacks for messages sent through XMPP, control messages, upstream messages. 274 | func (c *xmppGcmClient) listen(h MessageHandler, stop <-chan bool) error { 275 | if stop != nil { 276 | go func() { 277 | select { 278 | case <-stop: 279 | // Set isClosed to 0 so we don't trigger an error when returning. 280 | c.Lock() 281 | c.XmppClient.Close() 282 | c.isClosed = true 283 | c.Unlock() 284 | 285 | // Remove client from cache. 286 | xmppClients.Lock() 287 | delete(xmppClients.m, c.senderID) 288 | xmppClients.Unlock() 289 | } 290 | }() 291 | } 292 | for { 293 | stanza, err := c.XmppClient.Recv() 294 | if err != nil { 295 | c.RLock() 296 | defer c.RUnlock() 297 | // If client is closed we can't return without error. 298 | if c.isClosed { 299 | return nil 300 | } 301 | // This is likely fatal, so return. 302 | return fmt.Errorf("error on Recv>%v", err) 303 | } 304 | v, ok := stanza.(xmpp.Chat) 305 | if !ok { 306 | continue 307 | } 308 | switch v.Type { 309 | case "": 310 | cm := &CcsMessage{} 311 | err = json.Unmarshal([]byte(v.Other[0]), cm) 312 | if err != nil { 313 | debug("Error unmarshaling ccs message: %v", err) 314 | continue 315 | } 316 | switch cm.MessageType { 317 | case CCSAck: 318 | c.messages.Lock() 319 | // ack for a sent message, delete it from log. 320 | if _, ok := c.messages.m[cm.MessageId]; ok { 321 | go h(*cm) 322 | delete(c.messages.m, cm.MessageId) 323 | } 324 | c.messages.Unlock() 325 | case CCSNack: 326 | // nack for a sent message, retry if retryable error, bubble up otherwise. 327 | if retryableErrors[cm.Error] { 328 | c.retryMessage(*cm, h) 329 | } else { 330 | c.messages.Lock() 331 | if _, ok := c.messages.m[cm.MessageId]; ok { 332 | go h(*cm) 333 | delete(c.messages.m, cm.MessageId) 334 | } 335 | c.messages.Unlock() 336 | } 337 | default: 338 | debug("Unknown ccs message: %v", cm) 339 | } 340 | case "normal": 341 | cm := &CcsMessage{} 342 | err = json.Unmarshal([]byte(v.Other[0]), cm) 343 | if err != nil { 344 | debug("Error unmarshaling ccs message: %v", err) 345 | continue 346 | } 347 | switch cm.MessageType { 348 | case CCSControl: 349 | // TODO(silvano): create a new connection, drop the old one 'after a while' 350 | debug("control message! %v", cm) 351 | case CCSReceipt: 352 | debug("receipt! %v", cm) 353 | // Receipt message: send ack and pass to listener. 354 | origMessageID := strings.TrimPrefix(cm.MessageId, "dr2:") 355 | ack := XmppMessage{To: cm.From, MessageId: origMessageID, MessageType: CCSAck} 356 | c.send(ack) 357 | go h(*cm) 358 | default: 359 | debug("uknown upstream message! %v", cm) 360 | // Upstream message: send ack and pass to listener. 361 | ack := XmppMessage{To: cm.From, MessageId: cm.MessageId, MessageType: CCSAck} 362 | c.send(ack) 363 | go h(*cm) 364 | } 365 | case "error": 366 | debug("error response %v", v) 367 | default: 368 | debug("unknown message type %v", v) 369 | } 370 | } 371 | } 372 | 373 | //TODO(silvano): add flow control (max 100 pending messages at one time) 374 | // xmppGcmClient implementation to send a message through Gcm Xmpp server (ccs). 375 | func (c *xmppGcmClient) send(m XmppMessage) (string, int, error) { 376 | if m.MessageId == "" { 377 | m.MessageId = uuid.New() 378 | } 379 | c.messages.Lock() 380 | if _, ok := c.messages.m[m.MessageId]; !ok { 381 | b := newExponentialBackoff() 382 | if b.b.Min < ccsMinBackoff { 383 | b.setMin(ccsMinBackoff) 384 | } 385 | c.messages.m[m.MessageId] = &messageLogEntry{body: &m, backoff: b} 386 | } 387 | c.messages.Unlock() 388 | 389 | stanza := `%v` 390 | body, err := json.Marshal(m) 391 | if err != nil { 392 | return m.MessageId, 0, fmt.Errorf("could not unmarshal body of xmpp message>%v", err) 393 | } 394 | bs := string(body) 395 | 396 | debug("Sending XMPP: ", fmt.Sprintf(stanza, bs)) 397 | // Serialize wire access for thread safety. 398 | c.Lock() 399 | defer c.Unlock() 400 | bytes, err := c.XmppClient.SendOrg(fmt.Sprintf(stanza, bs)) 401 | return m.MessageId, bytes, err 402 | } 403 | 404 | // Retry sending a message with exponential backoff; if over limit, bubble up the failed message. 405 | func (c *xmppGcmClient) retryMessage(cm CcsMessage, h MessageHandler) { 406 | c.messages.RLock() 407 | defer c.messages.RUnlock() 408 | if me, ok := c.messages.m[cm.MessageId]; ok { 409 | if me.backoff.sendAnother() { 410 | go func(m *messageLogEntry) { 411 | m.backoff.wait() 412 | c.send(*m.body) 413 | }(me) 414 | } else { 415 | debug("Exponential backoff failed over limit for message: ", me) 416 | go h(cm) 417 | } 418 | } 419 | } 420 | 421 | // Interface to stub the http client in tests. 422 | type backoffProvider interface { 423 | sendAnother() bool 424 | setMin(min time.Duration) 425 | wait() 426 | } 427 | 428 | // Implementation of backoff provider using exponential backoff. 429 | type exponentialBackoff struct { 430 | b backoff.Backoff 431 | currentDelay time.Duration 432 | } 433 | 434 | // Factory method for exponential backoff, uses default values for Min and Max and 435 | // adds Jitter. 436 | func newExponentialBackoff() *exponentialBackoff { 437 | b := &backoff.Backoff{ 438 | Min: DefaultMinBackoff, 439 | Max: DefaultMaxBackoff, 440 | Jitter: true, 441 | } 442 | return &exponentialBackoff{b: *b, currentDelay: b.Duration()} 443 | } 444 | 445 | // Returns true if not over the retries limit 446 | func (eb exponentialBackoff) sendAnother() bool { 447 | return eb.currentDelay <= eb.b.Max 448 | } 449 | 450 | // Set the minumim delay for backoff 451 | func (eb *exponentialBackoff) setMin(min time.Duration) { 452 | eb.b.Min = min 453 | if (eb.currentDelay) < min { 454 | eb.currentDelay = min 455 | } 456 | } 457 | 458 | // Wait for the current value of backoff 459 | func (eb exponentialBackoff) wait() { 460 | time.Sleep(eb.currentDelay) 461 | eb.currentDelay = eb.b.Duration() 462 | } 463 | 464 | // Send a message using the HTTP GCM connection server. 465 | func SendHttp(apiKey string, m HttpMessage) (*HttpResponse, error) { 466 | c := &httpGcmClient{httpAddress, &http.Client{}, "0"} 467 | b := newExponentialBackoff() 468 | return sendHttp(apiKey, m, c, b) 469 | } 470 | 471 | // sendHttp sends an http message using exponential backoff, handling multicast replies. 472 | func sendHttp(apiKey string, m HttpMessage, c httpClient, b backoffProvider) (*HttpResponse, error) { 473 | // TODO(silvano): check this with responses for topic/notification group 474 | gcmResp := &HttpResponse{} 475 | var multicastId int 476 | targets, err := messageTargetAsStringsArray(m) 477 | if err != nil { 478 | return gcmResp, fmt.Errorf("error extracting target from message: %v", err) 479 | } 480 | // make a copy of the targets to keep track of results during retries 481 | localTo := make([]string, len(targets)) 482 | copy(localTo, targets) 483 | resultsState := &multicastResultsState{} 484 | for b.sendAnother() { 485 | gcmResp, err = c.send(apiKey, m) 486 | if err != nil { 487 | return gcmResp, fmt.Errorf("error sending request to GCM HTTP server: %v", err) 488 | } 489 | if len(gcmResp.Results) > 0 { 490 | doRetry, toRetry, err := checkResults(gcmResp.Results, localTo, *resultsState) 491 | multicastId = gcmResp.MulticastId 492 | if err != nil { 493 | return gcmResp, fmt.Errorf("error checking GCM results: %v", err) 494 | } 495 | if doRetry { 496 | retryAfter, err := time.ParseDuration(c.getRetryAfter()) 497 | if err != nil { 498 | b.setMin(retryAfter) 499 | } 500 | localTo = make([]string, len(toRetry)) 501 | copy(localTo, toRetry) 502 | if m.RegistrationIds != nil { 503 | m.RegistrationIds = toRetry 504 | } 505 | b.wait() 506 | continue 507 | } else { 508 | break 509 | } 510 | } else { 511 | break 512 | } 513 | } 514 | // if it was multicast, reconstruct response in case there have been retries 515 | if len(targets) > 1 { 516 | gcmResp = buildRespForMulticast(targets, *resultsState, multicastId) 517 | } 518 | return gcmResp, nil 519 | } 520 | 521 | // Builds the final response for a multicast message, in case there have been retries for 522 | // subsets of the original recipients. 523 | func buildRespForMulticast(to []string, mrs multicastResultsState, mid int) *HttpResponse { 524 | resp := &HttpResponse{} 525 | resp.MulticastId = mid 526 | resp.Results = make([]Result, len(to)) 527 | for i, regId := range to { 528 | result, ok := mrs[regId] 529 | if !ok { 530 | continue 531 | } 532 | resp.Results[i] = *result 533 | if result.MessageId != "" { 534 | resp.Success++ 535 | } else if result.Error != "" { 536 | resp.Failure++ 537 | } 538 | if result.RegistrationId != "" { 539 | resp.CanonicalIds++ 540 | } 541 | } 542 | return resp 543 | } 544 | 545 | // Transform the recipient in an array of strings if needed. 546 | func messageTargetAsStringsArray(m HttpMessage) ([]string, error) { 547 | if m.RegistrationIds != nil { 548 | return m.RegistrationIds, nil 549 | } else if m.To != "" { 550 | target := []string{m.To} 551 | return target, nil 552 | } 553 | target := []string{} 554 | return target, fmt.Errorf("can't find any valid target field in message.") 555 | } 556 | 557 | // For a multicast send, determines which errors can be retried. 558 | func checkResults(gcmResults []Result, recipients []string, resultsState multicastResultsState) (doRetry bool, toRetry []string, err error) { 559 | doRetry = false 560 | toRetry = []string{} 561 | for i := 0; i < len(gcmResults); i++ { 562 | result := gcmResults[i] 563 | regId := recipients[i] 564 | resultsState[regId] = &result 565 | if result.Error != "" { 566 | if retryableErrors[result.Error] { 567 | toRetry = append(toRetry, regId) 568 | if doRetry == false { 569 | doRetry = true 570 | } 571 | } 572 | } 573 | } 574 | return doRetry, toRetry, nil 575 | } 576 | 577 | // SendXmpp sends a message using the XMPP GCM connection server. 578 | func SendXmpp(senderId, apiKey string, m XmppMessage) (string, int, error) { 579 | c, err := newXmppGcmClient(senderId, apiKey) 580 | if err != nil { 581 | return "", 0, fmt.Errorf("error creating xmpp client>%v", err) 582 | } 583 | return c.send(m) 584 | } 585 | 586 | // Listen blocks and connects to GCM waiting for messages, calling the handler 587 | // for CCS message that can be of interest to the listener: upstream messages, delivery receipt 588 | // notifications, errors. An optional stop channel can be provided to 589 | // stop listening. 590 | func Listen(senderId, apiKey string, h MessageHandler, stop <-chan bool) error { 591 | cl, err := newXmppGcmClient(senderId, apiKey) 592 | if err != nil { 593 | return fmt.Errorf("error creating xmpp client>%v", err) 594 | } 595 | return cl.listen(h, stop) 596 | } 597 | 598 | // authHeader generates an authorization header value for an api key. 599 | func authHeader(apiKey string) string { 600 | return fmt.Sprintf("key=%v", apiKey) 601 | } 602 | 603 | // xmppUser generates an xmpp username from a sender ID. 604 | func xmppUser(senderId string) string { 605 | return senderId + "@" + xmppHost 606 | } 607 | -------------------------------------------------------------------------------- /gcm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gcm 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | "net/http/httptest" 22 | "net/url" 23 | "reflect" 24 | "testing" 25 | "time" 26 | ) 27 | 28 | func assertEqual(t *testing.T, v, e interface{}) { 29 | if v != e { 30 | t.Fatalf("%#v != %#v", v, e) 31 | } 32 | } 33 | 34 | func assertDeepEqual(t *testing.T, v, e interface{}) { 35 | if !reflect.DeepEqual(v, e) { 36 | t.Fatalf("%#v != %#v", v, e) 37 | } 38 | } 39 | 40 | type stubBackoff struct { 41 | } 42 | 43 | func (sb stubBackoff) sendAnother() bool { 44 | return true 45 | } 46 | 47 | func (sb stubBackoff) setMin(min time.Duration) { 48 | } 49 | 50 | func (sb stubBackoff) wait() { 51 | } 52 | 53 | var multicastTos = []string{"4", "8", "15", "16", "23", "42"} 54 | var multicastReply = []string{`{ "multicast_id": 216, 55 | "success": 3, 56 | "failure": 3, 57 | "canonical_ids": 1, 58 | "results": [ 59 | { "message_id": "1:0408" }, 60 | { "error": "Unavailable" }, 61 | { "error": "InternalServerError" }, 62 | { "message_id": "1:1517" }, 63 | { "message_id": "1:2342", "registration_id": "32" }, 64 | { "error": "NotRegistered"} 65 | ] 66 | }`, `{ "multicast_id": 217, 67 | "success": 2, 68 | "failure": 0, 69 | "canonical_ids": 0, 70 | "results": [ 71 | { "message_id": "1:0409" }, 72 | { "message_id": "1:1516" } 73 | ] 74 | }`} 75 | var expectedResp = `{"multicast_id": 217, 76 | "success": 5, 77 | "failure": 1, 78 | "canonical_ids": 1, 79 | "results": [ 80 | { "message_id": "1:0408" }, 81 | { "message_id": "1:0409" }, 82 | { "message_id": "1:1516" }, 83 | { "message_id": "1:1517" }, 84 | { "message_id": "1:2342", "registration_id": "32" }, 85 | { "error": "NotRegistered"} 86 | ] 87 | }` 88 | 89 | type stubHttpClient struct { 90 | InvocationNum int 91 | Requests []string 92 | } 93 | 94 | func (c *stubHttpClient) send(apiKey string, message HttpMessage) (*HttpResponse, error) { 95 | response := &HttpResponse{} 96 | err := json.Unmarshal([]byte(multicastReply[c.InvocationNum]), &response) 97 | c.InvocationNum++ 98 | return response, err 99 | } 100 | 101 | func (c stubHttpClient) getRetryAfter() string { 102 | return "" 103 | } 104 | 105 | var singleTargetMessage = &HttpMessage{To: "recipient"} 106 | var multipleTargetMessage = &HttpMessage{RegistrationIds: multicastTos} 107 | 108 | // Test send for http client 109 | func TestHttpClientSend(t *testing.T) { 110 | expectedRetryAfter := "10" 111 | var authHeader string 112 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 113 | w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "application/json") 114 | w.Header().Set(http.CanonicalHeaderKey("Retry-After"), expectedRetryAfter) 115 | w.WriteHeader(200) 116 | fmt.Fprintln(w, expectedResp) 117 | })) 118 | defer server.Close() 119 | 120 | transport := &http.Transport{ 121 | Proxy: func(req *http.Request) (*url.URL, error) { 122 | authHeader = req.Header.Get(http.CanonicalHeaderKey("Authorization")) 123 | return url.Parse(server.URL) 124 | }, 125 | } 126 | httpClient := &http.Client{Transport: transport} 127 | c := &httpGcmClient{server.URL, httpClient, "0"} 128 | response, error := c.send("apiKey", *singleTargetMessage) 129 | expectedAuthHeader := "key=apiKey" 130 | expResp := &HttpResponse{} 131 | err := json.Unmarshal([]byte(expectedResp), &expResp) 132 | if err != nil { 133 | t.Fatalf("error: %v", err) 134 | } 135 | assertEqual(t, authHeader, expectedAuthHeader) 136 | assertEqual(t, error, nil) 137 | assertDeepEqual(t, response, expResp) 138 | assertEqual(t, c.getRetryAfter(), expectedRetryAfter) 139 | } 140 | 141 | // test sending a GCM message through the HTTP connection server (includes backoff) 142 | func TestSendHttp(t *testing.T) { 143 | c := &stubHttpClient{} 144 | b := &stubBackoff{} 145 | expResp := &HttpResponse{} 146 | err := json.Unmarshal([]byte(expectedResp), &expResp) 147 | if err != nil { 148 | t.Fatalf("error: %v", err) 149 | } 150 | response, err := sendHttp("apiKey", *multipleTargetMessage, c, b) 151 | assertDeepEqual(t, err, nil) 152 | assertDeepEqual(t, response, expResp) 153 | } 154 | 155 | func TestBuildRespForMulticast(t *testing.T) { 156 | expResp := &HttpResponse{} 157 | err := json.Unmarshal([]byte(multicastReply[0]), &expResp) 158 | if err != nil { 159 | t.Fatalf("error: %v", err) 160 | } 161 | resultsState := &multicastResultsState{ 162 | "4": &Result{MessageId: "1:0408"}, 163 | "8": &Result{Error: "Unavailable"}, 164 | "15": &Result{Error: "InternalServerError"}, 165 | "16": &Result{MessageId: "1:1517"}, 166 | "23": &Result{MessageId: "1:2342", RegistrationId: "32"}, 167 | "42": &Result{Error: "NotRegistered"}, 168 | } 169 | resp := buildRespForMulticast(multicastTos, *resultsState, 216) 170 | assertDeepEqual(t, resp, expResp) 171 | } 172 | 173 | func TestMessageTargetAsStringArray(t *testing.T) { 174 | targets, err := messageTargetAsStringsArray(*singleTargetMessage) 175 | assertDeepEqual(t, targets, []string{"recipient"}) 176 | assertEqual(t, err, nil) 177 | targets, err = messageTargetAsStringsArray(*multipleTargetMessage) 178 | assertDeepEqual(t, targets, multicastTos) 179 | assertEqual(t, err, nil) 180 | invalidMessage := &HttpMessage{} 181 | targets, err = messageTargetAsStringsArray(*invalidMessage) 182 | assertDeepEqual(t, targets, []string{}) 183 | assertEqual(t, "can't find any valid target field in message.", err.Error()) 184 | } 185 | 186 | func TestCheckResults(t *testing.T) { 187 | response := &HttpResponse{} 188 | err := json.Unmarshal([]byte(multicastReply[0]), &response) 189 | if err != nil { 190 | t.Fatalf("error: %v", err) 191 | } 192 | resultsState := &multicastResultsState{} 193 | doRetry, toRetry, err := checkResults(response.Results, multicastTos, *resultsState) 194 | expectedToRetry := []string{"8", "15"} 195 | assertEqual(t, doRetry, true) 196 | assertDeepEqual(t, toRetry, expectedToRetry) 197 | expectedResultState := &multicastResultsState{ 198 | "4": &Result{MessageId: "1:0408"}, 199 | "8": &Result{Error: "Unavailable"}, 200 | "15": &Result{Error: "InternalServerError"}, 201 | "16": &Result{MessageId: "1:1517"}, 202 | "23": &Result{MessageId: "1:2342", RegistrationId: "32"}, 203 | "42": &Result{Error: "NotRegistered"}, 204 | } 205 | assertDeepEqual(t, resultsState, expectedResultState) 206 | } 207 | 208 | func TestXmppUser(t *testing.T) { 209 | assertEqual(t, xmppUser("b"), "b@gcm.googleapis.com") 210 | } 211 | --------------------------------------------------------------------------------