gntp.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. var net = require('net'),
  2. crypto = require('crypto'),
  3. format = require('util').format,
  4. fs = require('fs');
  5. var nl = '\r\n';
  6. /**
  7. * Create a new GNTP request of the given `type`.
  8. *
  9. * @param {String} type either NOTIFY or REGISTER
  10. * @api private
  11. */
  12. function GNTP(type, opts) {
  13. opts = opts || {};
  14. this.type = type;
  15. this.host = opts.host || 'localhost';
  16. this.port = opts.port || 23053;
  17. this.request = 'GNTP/1.0 ' + type + ' NONE' + nl;
  18. this.resources = [];
  19. this.attempts = 0;
  20. this.maxAttempts = 5;
  21. }
  22. /**
  23. * Build a response object from the given `resp` response string.
  24. *
  25. * The response object has a key/value pair for every header in the response, and
  26. * a `.state` property equal to either OK, ERROR, or CALLBACK.
  27. *
  28. * An example GNTP response:
  29. *
  30. * GNTP/1.0 -OK NONE\r\n
  31. * Response-Action: REGISTER\r\n
  32. * \r\n
  33. *
  34. * Which would parse to:
  35. *
  36. * { state: 'OK', 'Response-Action': 'REGISTER' }
  37. *
  38. * @param {String} resp
  39. * @return {Object}
  40. * @api private
  41. */
  42. GNTP.prototype.parseResp = function(resp) {
  43. var parsed = {}, head, body;
  44. resp = resp.slice(0, resp.indexOf(nl + nl)).split(nl);
  45. head = resp[0];
  46. body = resp.slice(1);
  47. parsed.state = head.match(/-(OK|ERROR|CALLBACK)/)[0].slice(1);
  48. body.forEach(function(ln) {
  49. ln = ln.split(': ');
  50. parsed[ln[0]] = ln[1];
  51. });
  52. return parsed;
  53. };
  54. /**
  55. * Call `GNTP.send()` with the given arguments after a certain delay.
  56. *
  57. * @api private
  58. */
  59. GNTP.prototype.retry = function() {
  60. var self = this,
  61. args = arguments;
  62. setTimeout(function() {
  63. self.send.apply(self, args);
  64. }, 750);
  65. };
  66. /**
  67. * Add a resource to the GNTP request.
  68. *
  69. * @param {Buffer} file
  70. * @return {String}
  71. * @api private
  72. */
  73. GNTP.prototype.addResource = function(file) {
  74. var id = crypto.createHash('md5').update(file).digest('hex'),
  75. header = 'Identifier: ' + id + nl + 'Length: ' + file.length + nl + nl;
  76. this.resources.push({ header: header, file: file });
  77. return 'x-growl-resource://' + id;
  78. };
  79. /**
  80. * Append another header `name` with a value of `val` to the request. If `val` is
  81. * undefined, the header will be left out.
  82. *
  83. * @param {String} name
  84. * @param {String} val
  85. * @api public
  86. */
  87. GNTP.prototype.add = function(name, val) {
  88. if (val === undefined)
  89. return;
  90. /* Handle icon files when they're image paths or Buffers. */
  91. if (/-Icon/.test(name) && !/^https?:\/\//.test(val) ) {
  92. if (/\.(png|gif|jpe?g)$/.test(val))
  93. val = this.addResource(fs.readFileSync(val));
  94. else if (val instanceof Buffer)
  95. val = this.addResource(val);
  96. }
  97. this.request += name + ': ' + val + nl;
  98. };
  99. /**
  100. * Append a newline to the request.
  101. *
  102. * @api public
  103. */
  104. GNTP.prototype.newline = function() {
  105. this.request += nl;
  106. };
  107. /**
  108. * Send the GNTP request, calling `callback` after successfully sending the
  109. * request.
  110. *
  111. * An example GNTP request:
  112. *
  113. * GNTP/1.0 REGISTER NONE\r\n
  114. * Application-Name: Growly.js\r\n
  115. * Notifications-Count: 1\r\n
  116. * \r\n
  117. * Notification-Name: default\r\n
  118. * Notification-Display-Name: Default Notification\r\n
  119. * Notification-Enabled: True\r\n
  120. * \r\n
  121. *
  122. * @param {Function} callback which will be passed the parsed response
  123. * @api public
  124. */
  125. GNTP.prototype.send = function(callback) {
  126. var self = this,
  127. socket = net.connect(this.port, this.host),
  128. resp = '';
  129. callback = callback || function() {};
  130. this.attempts += 1;
  131. socket.on('connect', function() {
  132. socket.write(self.request);
  133. self.resources.forEach(function(res) {
  134. socket.write(res.header);
  135. socket.write(res.file);
  136. socket.write(nl + nl);
  137. });
  138. });
  139. socket.on('data', function(data) {
  140. resp += data.toString();
  141. /* Wait until we have a complete response which is signaled by two CRLF's. */
  142. if (resp.slice(resp.length - 4) !== (nl + nl)) return;
  143. resp = self.parseResp(resp);
  144. /* We have to manually close the connection for certain responses; otherwise,
  145. reset `resp` to prepare for the next response chunk. */
  146. if (resp.state === 'ERROR' || resp.state === 'CALLBACK')
  147. socket.end();
  148. else
  149. resp = '';
  150. });
  151. socket.on('end', function() {
  152. /* Retry on 200 (timed out), 401 (unknown app), or 402 (unknown notification). */
  153. if (['200', '401', '402'].indexOf(resp['Error-Code']) >= 0) {
  154. if (self.attempts <= self.maxAttempts) {
  155. self.retry(callback);
  156. } else {
  157. var msg = 'GNTP request to "%s:%d" failed with error code %s (%s)';
  158. callback(new Error(format(msg, self.host, self.port, resp['Error-Code'], resp['Error-Description'])));
  159. }
  160. } else {
  161. callback(undefined, resp);
  162. }
  163. });
  164. socket.on('error', function() {
  165. callback(new Error(format('Error while sending GNTP request to "%s:%d"', self.host, self.port)));
  166. socket.destroy();
  167. });
  168. };
  169. module.exports = GNTP;