You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			244 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			244 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
| /* global window, dcodeIO, Event, textsecure, FileReader, WebSocketResource */
 | |
| 
 | |
| // eslint-disable-next-line func-names
 | |
| (function() {
 | |
|   /*
 | |
|      * WebSocket-Resources
 | |
|      *
 | |
|      * Create a request-response interface over websockets using the
 | |
|      * WebSocket-Resources sub-protocol[1].
 | |
|      *
 | |
|      * var client = new WebSocketResource(socket, function(request) {
 | |
|      *    request.respond(200, 'OK');
 | |
|      * });
 | |
|      *
 | |
|      * client.sendRequest({
 | |
|      *    verb: 'PUT',
 | |
|      *    path: '/v1/messages',
 | |
|      *    body: '{ some: "json" }',
 | |
|      *    success: function(message, status, request) {...},
 | |
|      *    error: function(message, status, request) {...}
 | |
|      * });
 | |
|      *
 | |
|      * 1. https://github.com/signalapp/WebSocket-Resources
 | |
|      *
 | |
|      */
 | |
| 
 | |
|   const Request = function Request(options) {
 | |
|     this.verb = options.verb || options.type;
 | |
|     this.path = options.path || options.url;
 | |
|     this.headers = options.headers;
 | |
|     this.body = options.body || options.data;
 | |
|     this.success = options.success;
 | |
|     this.error = options.error;
 | |
|     this.id = options.id;
 | |
| 
 | |
|     if (this.id === undefined) {
 | |
|       const bits = new Uint32Array(2);
 | |
|       window.crypto.getRandomValues(bits);
 | |
|       this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
 | |
|     }
 | |
| 
 | |
|     if (this.body === undefined) {
 | |
|       this.body = null;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const IncomingWebSocketRequest = function IncomingWebSocketRequest(options) {
 | |
|     const request = new Request(options);
 | |
|     const { socket } = options;
 | |
| 
 | |
|     this.verb = request.verb;
 | |
|     this.path = request.path;
 | |
|     this.body = request.body;
 | |
|     this.headers = request.headers;
 | |
| 
 | |
|     this.respond = (status, message) => {
 | |
|       socket.send(
 | |
|         new textsecure.protobuf.WebSocketMessage({
 | |
|           type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
 | |
|           response: { id: request.id, message, status },
 | |
|         })
 | |
|           .encode()
 | |
|           .toArrayBuffer()
 | |
|       );
 | |
|     };
 | |
|   };
 | |
| 
 | |
|   const outgoing = {};
 | |
|   const OutgoingWebSocketRequest = function OutgoingWebSocketRequest(
 | |
|     options,
 | |
|     socket
 | |
|   ) {
 | |
|     const request = new Request(options);
 | |
|     outgoing[request.id] = request;
 | |
|     socket.send(
 | |
|       new textsecure.protobuf.WebSocketMessage({
 | |
|         type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
 | |
|         request: {
 | |
|           verb: request.verb,
 | |
|           path: request.path,
 | |
|           body: request.body,
 | |
|           headers: request.headers,
 | |
|           id: request.id,
 | |
|         },
 | |
|       })
 | |
|         .encode()
 | |
|         .toArrayBuffer()
 | |
|     );
 | |
|   };
 | |
| 
 | |
|   window.WebSocketResource = function WebSocketResource(socket, opts = {}) {
 | |
|     let { handleRequest } = opts;
 | |
|     if (typeof handleRequest !== 'function') {
 | |
|       handleRequest = request => request.respond(404, 'Not found');
 | |
|     }
 | |
|     this.sendRequest = options => new OutgoingWebSocketRequest(options, socket);
 | |
| 
 | |
|     // eslint-disable-next-line no-param-reassign
 | |
|     socket.onmessage = socketMessage => {
 | |
|       const blob = socketMessage.data;
 | |
|       const handleArrayBuffer = buffer => {
 | |
|         const message = textsecure.protobuf.WebSocketMessage.decode(buffer);
 | |
|         if (
 | |
|           message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
 | |
|         ) {
 | |
|           handleRequest(
 | |
|             new IncomingWebSocketRequest({
 | |
|               verb: message.request.verb,
 | |
|               path: message.request.path,
 | |
|               body: message.request.body,
 | |
|               headers: message.request.headers,
 | |
|               id: message.request.id,
 | |
|               socket,
 | |
|             })
 | |
|           );
 | |
|         } else if (
 | |
|           message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
 | |
|         ) {
 | |
|           const { response } = message;
 | |
|           const request = outgoing[response.id];
 | |
|           if (request) {
 | |
|             request.response = response;
 | |
|             let callback = request.error;
 | |
|             if (response.status >= 200 && response.status < 300) {
 | |
|               callback = request.success;
 | |
|             }
 | |
| 
 | |
|             if (typeof callback === 'function') {
 | |
|               callback(response.message, response.status, request);
 | |
|             }
 | |
|           } else {
 | |
|             throw new Error(
 | |
|               `Received response for unknown request ${message.response.id}`
 | |
|             );
 | |
|           }
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       if (blob instanceof ArrayBuffer) {
 | |
|         handleArrayBuffer(blob);
 | |
|       } else {
 | |
|         const reader = new FileReader();
 | |
|         reader.onload = () => handleArrayBuffer(reader.result);
 | |
|         reader.readAsArrayBuffer(blob);
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     if (opts.keepalive) {
 | |
|       this.keepalive = new KeepAlive(this, {
 | |
|         path: opts.keepalive.path,
 | |
|         disconnect: opts.keepalive.disconnect,
 | |
|       });
 | |
|       const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
 | |
|       socket.addEventListener('open', resetKeepAliveTimer);
 | |
|       socket.addEventListener('message', resetKeepAliveTimer);
 | |
|       socket.addEventListener(
 | |
|         'close',
 | |
|         this.keepalive.stop.bind(this.keepalive)
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     socket.addEventListener('close', () => {
 | |
|       this.closed = true;
 | |
|     });
 | |
| 
 | |
|     this.close = (code = 3000, reason) => {
 | |
|       if (this.closed) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       window.log.info('WebSocketResource.close()');
 | |
|       if (this.keepalive) {
 | |
|         this.keepalive.stop();
 | |
|       }
 | |
| 
 | |
|       socket.close(code, reason);
 | |
|       // eslint-disable-next-line no-param-reassign
 | |
|       socket.onmessage = null;
 | |
| 
 | |
|       // On linux the socket can wait a long time to emit its close event if we've
 | |
|       //   lost the internet connection. On the order of minutes. This speeds that
 | |
|       //   process up.
 | |
|       setTimeout(() => {
 | |
|         if (this.closed) {
 | |
|           return;
 | |
|         }
 | |
|         this.closed = true;
 | |
| 
 | |
|         window.log.warn('Dispatching our own socket close event');
 | |
|         const ev = new Event('close');
 | |
|         ev.code = code;
 | |
|         ev.reason = reason;
 | |
|         this.dispatchEvent(ev);
 | |
|       }, 1000);
 | |
|     };
 | |
|   };
 | |
|   window.WebSocketResource.prototype = new textsecure.EventTarget();
 | |
| 
 | |
|   function KeepAlive(websocketResource, opts = {}) {
 | |
|     if (websocketResource instanceof WebSocketResource) {
 | |
|       this.path = opts.path;
 | |
|       if (this.path === undefined) {
 | |
|         this.path = '/';
 | |
|       }
 | |
|       this.disconnect = opts.disconnect;
 | |
|       if (this.disconnect === undefined) {
 | |
|         this.disconnect = true;
 | |
|       }
 | |
|       this.wsr = websocketResource;
 | |
|     } else {
 | |
|       throw new TypeError('KeepAlive expected a WebSocketResource');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   KeepAlive.prototype = {
 | |
|     constructor: KeepAlive,
 | |
|     stop() {
 | |
|       clearTimeout(this.keepAliveTimer);
 | |
|       clearTimeout(this.disconnectTimer);
 | |
|     },
 | |
|     reset() {
 | |
|       clearTimeout(this.keepAliveTimer);
 | |
|       clearTimeout(this.disconnectTimer);
 | |
|       this.keepAliveTimer = setTimeout(() => {
 | |
|         if (this.disconnect) {
 | |
|           // automatically disconnect if server doesn't ack
 | |
|           this.disconnectTimer = setTimeout(() => {
 | |
|             clearTimeout(this.keepAliveTimer);
 | |
|             this.wsr.close(3001, 'No response to keepalive request');
 | |
|           }, 1000);
 | |
|         } else {
 | |
|           this.reset();
 | |
|         }
 | |
|         window.log.info('Sending a keepalive message');
 | |
|         this.wsr.sendRequest({
 | |
|           verb: 'GET',
 | |
|           path: this.path,
 | |
|           success: this.reset.bind(this),
 | |
|         });
 | |
|       }, 55000);
 | |
|     },
 | |
|   };
 | |
| })();
 |