PageRenderTime 26ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/push/api/parts/gcm/index.js

https://gitlab.com/ar2rsawseen/countly-server
JavaScript | 251 lines | 206 code | 45 blank | 0 comment | 51 complexity | 3bcf3c71a8c7c0e86861eed9d42a43d3 MD5 | raw file
  1. 'use strict';
  2. const log = require('../../../../../api/utils/log.js')('push:gcm'),
  3. https = require('https'),
  4. EventEmitter = require('events');
  5. const MAX_QUEUE = 300,
  6. MAX_BATCH = 100;
  7. class ConnectionResource extends EventEmitter {
  8. constructor(key) {
  9. super();
  10. log.w('New GCM connection %j', arguments);
  11. this._key = key;
  12. this.devices = [];
  13. this.ids = [];
  14. this.inFlight = 0;
  15. this.onSocket = (s) => {
  16. this.socket = s;
  17. };
  18. this.onError = (e) => {
  19. log.e('socket error %j', e);
  20. };
  21. }
  22. init() {
  23. this.options = {
  24. hostname: 'android.googleapis.com',
  25. port: 443,
  26. path: '/gcm/send',
  27. method: 'POST',
  28. headers: {
  29. 'Accept': 'application/json',
  30. 'Content-Type': 'application/json',
  31. 'Authorization': 'key=' + this._key,
  32. },
  33. };
  34. this.agent = new https.Agent(this.options);
  35. this.agent.maxSockets = 1;
  36. this.options.agent = this.agent;
  37. return Promise.resolve();
  38. }
  39. resolve() {
  40. return Promise.resolve();
  41. }
  42. init_connection() {
  43. return Promise.resolve();
  44. }
  45. feed (array) {
  46. try {
  47. this.queue += array.length;
  48. array.forEach(u => {
  49. this.ids[u[2]].push([u[0], 1]);
  50. this.devices[u[2]].push(u[1]);
  51. });
  52. this.serviceImmediate();
  53. log.d('[%d]: Fed %d, now %d', process.pid, array.length, this.queue);
  54. }catch(e) { log.e(e, e.stack); }
  55. return (this.lastFeed = array.length);
  56. }
  57. send(msgs, feeder, status) {
  58. this.messages = msgs;
  59. this.devices = msgs.map(_ => []);
  60. this.ids = msgs.map(_ => []);
  61. this.queue = 0;
  62. this.lastFeed = -1;
  63. this.feeder = feeder;
  64. this.statuser = status;
  65. this.serviceImmediate();
  66. return new Promise((resolve, reject) => {
  67. this.promiseResolve = resolve;
  68. this.promiseReject = reject;
  69. });
  70. }
  71. serviceImmediate () {
  72. if (!this._servicing) {
  73. this._servicing = true;
  74. setImmediate(this.service.bind(this));
  75. }
  76. }
  77. serviceWithTimeout () {
  78. if (!this._servicing) {
  79. this._servicing = true;
  80. setTimeout(this.service.bind(this), 1000);
  81. }
  82. }
  83. service() {
  84. log.d('[%d]: Servicing', process.pid);
  85. this._servicing = false;
  86. if (this.agent === null || this._closed) {
  87. return;
  88. }
  89. if (this.lastFeed !== 0 && this.queue < MAX_QUEUE / 2) {
  90. this.feeder(MAX_QUEUE - this.queue);
  91. return;
  92. } else if (this.lastFeed === 0 && this.queue === 0) {
  93. return this.promiseResolve();
  94. }
  95. log.d('dbg %j', Math.max.apply(null, this.ids.map(ids => ids.length)));
  96. let lengths = this.ids.map(ids => ids.length),
  97. dataIndex = lengths.indexOf(Math.max.apply(Math, lengths)),
  98. devices = this.devices[dataIndex].splice(0, MAX_BATCH),
  99. ids = this.ids[dataIndex].splice(0, MAX_BATCH),
  100. message = this.messages[dataIndex];
  101. log.d('dataIndex %j', dataIndex);
  102. if (devices.length) {
  103. message.registration_ids = devices;
  104. log.d('sending %j', message);
  105. log.d('with %j', ids);
  106. let content = JSON.stringify(message);
  107. this.options.headers['Content-length'] = Buffer.byteLength(content, 'utf8');
  108. let req = https.request(this.options, (res) => {
  109. res.reply = '';
  110. res.on('data', d => { res.reply += d; });
  111. res.on('end', this.handle.bind(this, req, res, ids, devices));
  112. res.on('close', this.handle.bind(this, req, res, ids, devices));
  113. });
  114. req.on('socket', this.onSocket.bind(this));
  115. req.on('error', this.onError.bind(this));
  116. req.end(content);
  117. this.queue -= devices.length;
  118. } else {
  119. this.serviceWithTimeout();
  120. }
  121. }
  122. rejectAndClose(error) {
  123. this.promiseReject(error);
  124. this.close_connection();
  125. }
  126. handle(req, res, ids, devices) {
  127. if (req.handled || this._closed) { return; }
  128. req.handled = true;
  129. let code = res.statusCode,
  130. data = res.reply;
  131. log.d('[%d]: GCM handling %d', process.pid, code);
  132. log.d('[%d]: GCM data %j', process.pid, data);
  133. if (code >= 500) {
  134. this.rejectAndClose(code + ': GCM Unavailable');
  135. } else if (code === 401) {
  136. this.rejectAndClose(code + ': GCM Unauthorized');
  137. } else if (code === 400) {
  138. this.rejectAndClose(code + ': GCM Bad message');
  139. } else if (code !== 200) {
  140. this.rejectAndClose(code + ': Bad response code');
  141. } else {
  142. try {
  143. if (data && data[0] === '"' && data[data.length - 1] === '"') {
  144. data = data.substr(1, data.length - 2);
  145. log.d('GCM replaced quotes: %j', data);
  146. }
  147. var obj = JSON.parse(data);
  148. if (obj.failure === 0 && obj.canonical_ids === 0) {
  149. this.statuser(ids);
  150. } else if (obj.results) {
  151. obj.results.forEach((result, i) => {
  152. if (result.message_id && result.registration_id) {
  153. ids[i][1] = 2;
  154. ids[i][2] = result.registration_id;
  155. } else if (result.error === 'MessageTooBig') {
  156. this.rejectAndClose(code + ': GCM Message Too Big');
  157. } else if (result.error === 'InvalidDataKey') {
  158. this.rejectAndClose(code + ': Invalid Data Key');
  159. } else if (result.error === 'InvalidTtl') {
  160. this.rejectAndClose(code + ': Invalid Time To Live');
  161. } else if (result.error === 'InvalidTtl') {
  162. this.rejectAndClose(code + ': Invalid Time To Live');
  163. } else if (result.error === 'InvalidPackageName') {
  164. this.rejectAndClose(code + ': Invalid Package Name');
  165. } else if (result.error === 'Unavailable' || result.error === 'InternalServerError') {
  166. ids[i] = -499;
  167. ids.splice(i, 1);
  168. devices.splice(i, 1);
  169. } else if (result.error === 'MismatchSenderId') {
  170. ids[i][1] = -498;
  171. } else if (result.error === 'NotRegistered' || result.error === 'InvalidRegistration') {
  172. ids[i][1] = 0;
  173. } else if (result.error) {
  174. log.w('Unknown GCM error: %j', process.pid, result.error);
  175. ids[i][1] = -497;
  176. }
  177. });
  178. this.statuser(ids);
  179. }
  180. this.serviceImmediate();
  181. } catch (e) {
  182. ids.forEach(i => i[1] = -1);
  183. this.statuser(ids);
  184. log.w('[%d]: Bad response from GCM: %j / %j / %j', process.pid, code, data, e);
  185. }
  186. }
  187. this._servicing = false;
  188. }
  189. close_connection() {
  190. log.i('[%d]: Closing GCM connection', process.pid);
  191. this._closed = true;
  192. if (this.socket) {
  193. this.socket.emit('agentRemove');
  194. this.socket = null;
  195. }
  196. if (this.agent) {
  197. this.agent.destroy();
  198. this.agent = null;
  199. }
  200. this.emit('closed');
  201. return Promise.resolve();
  202. }
  203. }
  204. module.exports = {
  205. ConnectionResource: ConnectionResource
  206. };