websocket-resources.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /*
  2. * vim: ts=4:sw=4:expandtab
  3. */
  4. ;(function(){
  5. 'use strict';
  6. /*
  7. * WebSocket-Resources
  8. *
  9. * Create a request-response interface over websockets using the
  10. * WebSocket-Resources sub-protocol[1].
  11. *
  12. * var client = new WebSocketResource(socket, function(request) {
  13. * request.respond(200, 'OK');
  14. * });
  15. *
  16. * client.sendRequest({
  17. * verb: 'PUT',
  18. * path: '/v1/messages',
  19. * body: '{ some: "json" }',
  20. * success: function(message, status, request) {...},
  21. * error: function(message, status, request) {...}
  22. * });
  23. *
  24. * 1. https://github.com/WhisperSystems/WebSocket-Resources
  25. *
  26. */
  27. var Request = function(options) {
  28. this.verb = options.verb || options.type;
  29. this.path = options.path || options.url;
  30. this.body = options.body || options.data;
  31. this.success = options.success;
  32. this.error = options.error;
  33. this.id = options.id;
  34. if (this.id === undefined) {
  35. var bits = new Uint32Array(2);
  36. window.crypto.getRandomValues(bits);
  37. this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
  38. }
  39. if (this.body === undefined) {
  40. this.body = null;
  41. }
  42. };
  43. var IncomingWebSocketRequest = function(options) {
  44. var request = new Request(options);
  45. var socket = options.socket;
  46. this.verb = request.verb;
  47. this.path = request.path;
  48. this.body = request.body;
  49. this.respond = function(status, message) {
  50. socket.send(
  51. new textsecure.protobuf.WebSocketMessage({
  52. type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
  53. response: { id: request.id, message: message, status: status }
  54. }).encode().toArrayBuffer()
  55. );
  56. };
  57. };
  58. var outgoing = {};
  59. var OutgoingWebSocketRequest = function(options, socket) {
  60. var request = new Request(options);
  61. outgoing[request.id] = request;
  62. socket.send(
  63. new textsecure.protobuf.WebSocketMessage({
  64. type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
  65. request: {
  66. verb : request.verb,
  67. path : request.path,
  68. body : request.body,
  69. id : request.id
  70. }
  71. }).encode().toArrayBuffer()
  72. );
  73. };
  74. window.WebSocketResource = function(socket, opts) {
  75. opts = opts || {};
  76. var handleRequest = opts.handleRequest;
  77. if (typeof handleRequest !== 'function') {
  78. handleRequest = function(request) {
  79. request.respond(404, 'Not found');
  80. };
  81. }
  82. this.sendRequest = function(options) {
  83. return new OutgoingWebSocketRequest(options, socket);
  84. };
  85. socket.onmessage = function(socketMessage) {
  86. var blob = socketMessage.data;
  87. var reader = new FileReader();
  88. reader.onload = function() {
  89. var message = textsecure.protobuf.WebSocketMessage.decode(reader.result);
  90. if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) {
  91. handleRequest(
  92. new IncomingWebSocketRequest({
  93. verb : message.request.verb,
  94. path : message.request.path,
  95. body : message.request.body,
  96. id : message.request.id,
  97. socket : socket
  98. })
  99. );
  100. }
  101. else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) {
  102. var response = message.response;
  103. var request = outgoing[response.id];
  104. if (request) {
  105. request.response = response;
  106. var callback = request.error;
  107. if (response.status >= 200 && response.status < 300) {
  108. callback = request.success;
  109. }
  110. if (typeof callback === 'function') {
  111. callback(response.message, response.status, request);
  112. }
  113. } else {
  114. throw 'Received response for unknown request ' + message.response.id;
  115. }
  116. }
  117. };
  118. reader.readAsArrayBuffer(blob);
  119. };
  120. if (opts.keepalive) {
  121. var keepalive = new KeepAlive(this, {
  122. path : opts.keepalive.path,
  123. disconnect : opts.keepalive.disconnect
  124. });
  125. var resetKeepAliveTimer = keepalive.reset.bind(keepalive);
  126. socket.addEventListener('open', resetKeepAliveTimer);
  127. socket.addEventListener('message', resetKeepAliveTimer);
  128. socket.addEventListener('close', keepalive.stop.bind(keepalive));
  129. }
  130. this.close = function(code, reason) {
  131. if (!code) { code = 3000; }
  132. socket.close(code, reason);
  133. };
  134. };
  135. function KeepAlive(websocketResource, opts) {
  136. if (websocketResource instanceof WebSocketResource) {
  137. opts = opts || {};
  138. this.path = opts.path;
  139. if (this.path === undefined) {
  140. this.path = '/';
  141. }
  142. this.disconnect = opts.disconnect;
  143. if (this.disconnect === undefined) {
  144. this.disconnect = true;
  145. }
  146. this.wsr = websocketResource;
  147. } else {
  148. throw new TypeError('KeepAlive expected a WebSocketResource');
  149. }
  150. }
  151. KeepAlive.prototype = {
  152. constructor: KeepAlive,
  153. stop: function() {
  154. clearTimeout(this.keepAliveTimer);
  155. clearTimeout(this.disconnectTimer);
  156. },
  157. reset: function() {
  158. clearTimeout(this.keepAliveTimer);
  159. clearTimeout(this.disconnectTimer);
  160. this.keepAliveTimer = setTimeout(function() {
  161. this.wsr.sendRequest({
  162. verb: 'GET',
  163. path: this.path,
  164. success: this.reset.bind(this)
  165. });
  166. if (this.disconnect) {
  167. // automatically disconnect if server doesn't ack
  168. this.disconnectTimer = setTimeout(function() {
  169. clearTimeout(this.keepAliveTimer);
  170. this.wsr.close(3001, 'No response to keepalive request');
  171. }.bind(this), 1000);
  172. } else {
  173. this.reset();
  174. }
  175. }.bind(this), 55000);
  176. },
  177. };
  178. }());