saltyrtc-task-webrtc
Version:
A SaltyRTC WebRTC task implementation.
30 lines (29 loc) • 19.3 kB
JavaScript
/**
* saltyrtc-task-webrtc v0.9.1
* A SaltyRTC WebRTC task implementation.
* https://github.com/saltyrtc/saltyrtc-task-webrtc-js#readme
*
* Copyright (C) 2016 Threema GmbH
*
* This software may be modified and distributed under the terms
* of the MIT license:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";!function(e){var n=(function(){function e(e){this.value=e}function n(n){function t(e,n){return new Promise(function(t,r){var s={key:e,arg:n,resolve:t,reject:r,next:null};o?o=o.next=s:(a=o=s,i(e,n))})}function i(t,a){try{var o=n[t](a),s=o.value;s instanceof e?Promise.resolve(s.value).then(function(e){i("next",e)},function(e){i("throw",e)}):r(o.done?"return":"normal",o.value)}catch(e){r("throw",e)}}function r(e,n){switch(e){case"return":a.resolve({value:n,done:!0});break;case"throw":a.reject(n);break;default:a.resolve({value:n,done:!1})}a=a.next,a?i(a.key,a.arg):o=null}var a,o;this._invoke=t,"function"!=typeof n.return&&(this.return=void 0)}return"function"==typeof Symbol&&Symbol.asyncIterator&&(n.prototype[Symbol.asyncIterator]=function(){return this}),n.prototype.next=function(e){return this._invoke("next",e)},n.prototype.throw=function(e){return this._invoke("throw",e)},n.prototype.return=function(e){return this._invoke("return",e)},{wrap:function(e){return function(){return new n(e.apply(this,arguments))}},await:function(n){return new e(n)}}}(),function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}),t=function(){function e(e,n){for(var t=0;t<n.length;t++){var i=n[t];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(n,t,i){return t&&e(n.prototype,t),i&&e(n,i),n}}(),i=function(e){if(Array.isArray(e)){for(var n=0,t=Array(e.length);n<e.length;n++)t[n]=e[n];return t}return Array.from(e)},r=function(){function e(t,i,r,a){n(this,e),this._cookie=t,this._overflow=r,this._sequenceNumber=a,this._channelId=i}return t(e,[{key:"toArrayBuffer",value:function(){var n=new ArrayBuffer(e.TOTAL_LENGTH),t=new Uint8Array(n);t.set(this._cookie.bytes);var i=new DataView(n);return i.setUint16(16,this._channelId),i.setUint16(18,this._overflow),i.setUint32(20,this._sequenceNumber),n}},{key:"toUint8Array",value:function(){return new Uint8Array(this.toArrayBuffer())}},{key:"cookie",get:function(){return this._cookie}},{key:"overflow",get:function(){return this._overflow}},{key:"sequenceNumber",get:function(){return this._sequenceNumber}},{key:"combinedSequenceNumber",get:function(){return(this._overflow<<32)+this._sequenceNumber}},{key:"channelId",get:function(){return this._channelId}}],[{key:"fromArrayBuffer",value:function(n){if(n.byteLength!=e.TOTAL_LENGTH)throw"bad-packet-length";var t=new DataView(n),i=new saltyrtcClient.Cookie(new Uint8Array(n,0,16)),r=t.getUint16(16),a=t.getUint16(18),o=t.getUint32(20);return new e(i,r,a,o)}}]),e}();r.TOTAL_LENGTH=24;var a=function(){function e(t,i){var a=this;if(n(this,e),this.logTag="[SaltyRTC.SecureDataChannel]",this.messageNumber=0,this.chunkCount=0,this.onChunk=function(n){return console.debug(a.logTag,"Received chunk"),n.data instanceof Blob?void console.warn(a.logTag,"Received message in blob format, which is not currently supported."):"string"==typeof n.data?void console.warn(a.logTag,"Received message in string format, which is not currently supported."):n.data instanceof ArrayBuffer?(a.unchunker.add(n.data,n),void(a.chunkCount++>e.CHUNK_COUNT_GC&&(a.unchunker.gc(e.CHUNK_MAX_AGE),a.chunkCount=0))):void console.warn(a.logTag,"Received message in unsupported format. Please send ArrayBuffer objects.")},this.onEncryptedMessage=function(e,n){if(void 0!==a._onmessage){console.debug(a.logTag,"Decrypting incoming data...");var t=n[n.length-1],i={};for(var o in t)i[o]=t[o];var s=saltyrtcClient.Box.fromUint8Array(new Uint8Array(e),nacl.box.nonceLength);try{a.validateNonce(r.fromArrayBuffer(s.nonce.buffer))}catch(e){return console.error(a.logTag,"Invalid nonce:",e),console.error(a.logTag,"Closing data channel"),a.close(),void a.task.close(saltyrtcClient.CloseCode.ProtocolError)}var c=a.task.getSignaling().decryptFromPeer(s);i.data=c.buffer.slice(c.byteOffset,c.byteOffset+c.byteLength),a._onmessage.bind(a.dc)(i)}},"arraybuffer"!==t.binaryType)throw new Error("Currently SaltyRTC can only handle data channels with `binaryType` set to `arraybuffer`.");if(this.dc=t,this.task=i,this.cookiePair=new saltyrtcClient.CookiePair,this.csnPair=new saltyrtcClient.CombinedSequencePair,this.chunkSize=this.task.getMaxPacketSize(),null===this.chunkSize)throw new Error("Could not determine max chunk size");0===this.chunkSize?this.dc.onmessage=function(e){return a.onEncryptedMessage(e.data,[e])}:(this.unchunker=new chunkedDc.Unchunker,this.unchunker.onMessage=this.onEncryptedMessage,this.dc.onmessage=this.onChunk)}return t(e,[{key:"send",value:function(e){var n=void 0;if("string"==typeof e)throw new Error("SecureDataChannel can only handle binary data.");if(e instanceof Blob)throw new Error("SecureDataChannel does not currently support Blob data. Please pass in an ArrayBuffer or a typed array (e.g. Uint8Array).");if(e instanceof Int8Array||e instanceof Uint8ClampedArray||e instanceof Int16Array||e instanceof Uint16Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Float32Array||e instanceof Float64Array||e instanceof DataView){var t=e.byteOffset||0,i=t+(e.byteLength||e.buffer.byteLength);n=e.buffer.slice(t,i)}else if(e instanceof Uint8Array)n=e.buffer;else{if(!(e instanceof ArrayBuffer))throw new Error("Unknown data type. Please pass in an ArrayBuffer or a typed array (e.g. Uint8Array).");n=e}var r=this.encryptData(new Uint8Array(n)),a=r.toUint8Array();if(0===this.chunkSize)this.dc.send(a);else{var o=new chunkedDc.Chunker(this.messageNumber++,a,this.chunkSize),s=!0,c=!1,d=void 0;try{for(var l,u=o[Symbol.iterator]();!(s=(l=u.next()).done);s=!0){var h=l.value;this.dc.send(h)}}catch(e){c=!0,d=e}finally{try{!s&&u.return&&u.return()}finally{if(c)throw d}}}}},{key:"encryptData",value:function(e){var n=this.csnPair.ours.next(),t=new r(this.cookiePair.ours,this.dc.id,n.overflow,n.sequenceNumber),i=this.task.getSignaling().encryptForPeer(e,t.toUint8Array());return i}},{key:"validateNonce",value:function(e){if(e.cookie.equals(this.cookiePair.ours))throw new Error("Local and remote cookie are equal");if(null===this.cookiePair.theirs||void 0===this.cookiePair.theirs)this.cookiePair.theirs=e.cookie;else if(!e.cookie.equals(this.cookiePair.theirs))throw new Error("Remote cookie changed");if(null!=this.lastIncomingCsn&&e.combinedSequenceNumber==this.lastIncomingCsn)throw new Error("CSN reuse detected!");if(e.channelId!=this.dc.id)throw new Error("Data channel id in nonce does not match actual data channel id");this.lastIncomingCsn=e.combinedSequenceNumber}},{key:"close",value:function(){this.dc.close()}},{key:"addEventListener",value:function(e,n,t){if("message"===e)throw new Error("addEventListener on message events is not currently supported by SaltyRTC.");this.dc.addEventListener(e,n,t)}},{key:"removeEventListener",value:function(e,n,t){if("message"===e)throw new Error("removeEventListener on message events is not currently supported by SaltyRTC.");this.dc.removeEventListener(e,n,t)}},{key:"dispatchEvent",value:function(e){return this.dc.dispatchEvent(e)}},{key:"label",get:function(){return this.dc.label}},{key:"ordered",get:function(){return this.dc.ordered}},{key:"maxPacketLifeTime",get:function(){return this.dc.maxPacketLifeTime}},{key:"maxRetransmits",get:function(){return this.dc.maxRetransmits}},{key:"protocol",get:function(){return this.dc.protocol}},{key:"negotiated",get:function(){return this.dc.negotiated}},{key:"id",get:function(){return this.dc.id}},{key:"readyState",get:function(){return this.dc.readyState}},{key:"bufferedAmount",get:function(){return this.dc.bufferedAmount}},{key:"bufferedAmountLowThreshold",get:function(){return this.dc.bufferedAmountLowThreshold},set:function(e){this.dc.bufferedAmountLowThreshold=e}},{key:"binaryType",get:function(){return this.dc.binaryType},set:function(e){this.dc.binaryType=e}},{key:"onopen",get:function(){return this.dc.onopen},set:function(e){this.dc.onopen=e}},{key:"onbufferedamountlow",get:function(){return this.dc.onbufferedamountlow},set:function(e){this.dc.onbufferedamountlow=e}},{key:"onerror",get:function(){return this.dc.onerror},set:function(e){this.dc.onerror=e}},{key:"onclose",get:function(){return this.dc.onclose},set:function(e){this.dc.onclose=e}},{key:"onmessage",get:function(){return this.dc.onmessage},set:function(e){this._onmessage=e}}]),e}();a.CHUNK_COUNT_GC=32,a.CHUNK_MAX_AGE=6e4;var o=function(){function e(){var t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:e.DEFAULT_MAX_PACKET_SIZE;n(this,e),this.initialized=!1,this.exclude=new Set,this.doHandover=!0,this.sdc=null,this.eventRegistry=new saltyrtcClient.EventRegistry,this.candidates=[],this.sendCandidatesTimeout=null,this.doHandover=t,this.requestedMaxPacketSize=i}return t(e,[{key:"init",value:function(n,t){this.processExcludeList(t[e.FIELD_EXCLUDE]),this.processMaxPacketSize(t[e.FIELD_MAX_PACKET_SIZE]),this.processHandover(t[e.FIELD_HANDOVER]),this.signaling=n,this.initialized=!0}},{key:"processExcludeList",value:function(e){var n=!0,t=!1,i=void 0;try{for(var r,a=e[Symbol.iterator]();!(n=(r=a.next()).done);n=!0){var o=r.value;this.exclude.add(o)}}catch(e){t=!0,i=e}finally{try{!n&&a.return&&a.return()}finally{if(t)throw i}}for(var s=0;s<=65535;s++)if(!this.exclude.has(s)){this.sdcId=s;break}if(void 0===this.sdcId&&this.doHandover===!0)throw new Error("Exclude list is too big, no free data channel id can be found")}},{key:"processMaxPacketSize",value:function(n){if(!Number.isInteger(n))throw new RangeError(e.FIELD_MAX_PACKET_SIZE+" field must be an integer");if(n<0)throw new RangeError(e.FIELD_MAX_PACKET_SIZE+" field must be positive");0===n&&0===this.requestedMaxPacketSize?this.negotiatedMaxPacketSize=0:0===n||0===this.requestedMaxPacketSize?this.negotiatedMaxPacketSize=Math.max(n,this.requestedMaxPacketSize):this.negotiatedMaxPacketSize=Math.min(n,this.requestedMaxPacketSize),console.debug(this.logTag,"Max packet size: We requested",this.requestedMaxPacketSize,"bytes, peer requested",n,"bytes. Using",this.negotiatedMaxPacketSize+".")}},{key:"processHandover",value:function(e){e===!1&&(this.doHandover=!1)}},{key:"onPeerHandshakeDone",value:function(){}},{key:"onTaskMessage",value:function(e){switch(console.debug(this.logTag,"New task message arrived: "+e.type),e.type){case"offer":if(this.validateOffer(e)!==!0)return;this.emit({type:"offer",data:e.offer});break;case"answer":if(this.validateAnswer(e)!==!0)return;this.emit({type:"answer",data:e.answer});break;case"candidates":if(this.validateCandidates(e)!==!0)return;this.emit({type:"candidates",data:e.candidates});break;case"handover":if(this.doHandover===!1){console.error(this.logTag,"Received unexpected handover message from peer"),this.signaling.resetConnection(saltyrtcClient.CloseCode.ProtocolError);break}this.signaling.handoverState.local===!1&&this.sendHandover(),this.signaling.handoverState.peer=!0,this.signaling.handoverState.both&&console.info(this.logTag,"Handover to data channel finished");break;default:console.error(this.logTag,"Received message with unknown type:",e.type)}}},{key:"validateOffer",value:function(e){return void 0===e.offer?(console.warn(this.logTag,"Offer message does not contain offer"),!1):void 0!==e.offer.sdp||(console.warn(this.logTag,"Offer message does not contain offer sdp"),!1)}},{key:"validateAnswer",value:function(e){return void 0===e.answer?(console.warn(this.logTag,"Answer message does not contain answer"),!1):void 0!==e.answer.sdp||(console.warn(this.logTag,"Answer message does not contain answer sdp"),!1)}},{key:"validateCandidates",value:function(e){if(void 0===e.candidates)return console.warn(this.logTag,"Candidates message does not contain candidates"),!1;if(e.candidates.length<1)return console.warn(this.logTag,"Candidates message contains empty candidate list"),!1;var n=!0,t=!1,i=void 0;try{for(var r,a=e.candidates[Symbol.iterator]();!(n=(r=a.next()).done);n=!0){var o=r.value;if("string"!=typeof o.candidate&&!(o.candidate instanceof String))return console.warn(this.logTag,"Candidates message contains invalid candidate (candidate field)"),!1;if("string"!=typeof o.sdpMid&&!(o.sdpMid instanceof String)&&null!==o.sdpMid)return console.warn(this.logTag,"Candidates message contains invalid candidate (sdpMid field)"),!1;if(null!==o.sdpMLineIndex&&!Number.isInteger(o.sdpMLineIndex))return console.warn(this.logTag,"Candidates message contains invalid candidate (sdpMLineIndex field)"),!1}}catch(e){t=!0,i=e}finally{try{!n&&a.return&&a.return()}finally{if(t)throw i}}return!0}},{key:"sendSignalingMessage",value:function(e){if("task"!=this.signaling.getState())throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError,"Could not send signaling message: Signaling state is not open.");if(this.signaling.handoverState.local===!1)throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError,"Could not send signaling message: Handover hasn't happened yet.");this.sdc.send(e)}},{key:"getName",value:function(){return e.PROTOCOL_NAME}},{key:"getSupportedMessageTypes",value:function(){return["offer","answer","candidates","handover"]}},{key:"getMaxPacketSize",value:function(){return this.initialized===!0?this.negotiatedMaxPacketSize:null}},{key:"getData",value:function(){var n={};return n[e.FIELD_EXCLUDE]=Array.from(this.exclude.values()),n[e.FIELD_MAX_PACKET_SIZE]=this.requestedMaxPacketSize,n[e.FIELD_HANDOVER]=this.doHandover,n}},{key:"getSignaling",value:function(){return this.signaling}},{key:"sendOffer",value:function(e){console.debug(this.logTag,"Sending offer");try{this.signaling.sendTaskMessage({type:"offer",offer:{type:e.type,sdp:e.sdp}})}catch(e){"SignalingError"===e.name&&(console.error(this.logTag,"Could not send offer:",e.message),this.signaling.resetConnection(e.closeCode))}}},{key:"sendAnswer",value:function(e){console.debug(this.logTag,"Sending answer");try{this.signaling.sendTaskMessage({type:"answer",answer:{type:e.type,sdp:e.sdp}})}catch(e){"SignalingError"===e.name&&(console.error(this.logTag,"Could not send answer:",e.message),this.signaling.resetConnection(e.closeCode))}}},{key:"sendCandidate",value:function(e){this.sendCandidates([e])}},{key:"sendCandidates",value:function(n){var t,r=this;console.debug(this.logTag,"Buffering",n.length,"candidate(s)"),(t=this.candidates).push.apply(t,i(n));var a=function(){try{console.debug(r.logTag,"Sending",r.candidates.length,"candidate(s)"),r.signaling.sendTaskMessage({type:"candidates",candidates:r.candidates})}catch(e){"SignalingError"===e.name&&(console.error(r.logTag,"Could not send candidates:",e.message),r.signaling.resetConnection(e.closeCode))}finally{r.candidates=[],r.sendCandidatesTimeout=null}};null===this.sendCandidatesTimeout&&(this.sendCandidatesTimeout=window.setTimeout(a,e.CANDIDATE_BUFFERING_MS))}},{key:"handover",value:function(n){var t=this;if(console.debug(this.logTag,"Initiate handover"),this.doHandover===!1)return console.error(this.logTag,"Cannot do handover: Either us or our peer set handover=false"),!1;if(this.signaling.handoverState.any)return console.error(this.logTag,"Handover already in progress or finished"),!1;if(void 0===this.sdcId||null===this.sdcId)throw console.error(this.logTag,"Data channel id not set"),this.signaling.resetConnection(saltyrtcClient.CloseCode.InternalError),new Error("Data channel id not set");var i=n.createDataChannel(e.DC_LABEL,{id:this.sdcId,negotiated:!0,ordered:!0,protocol:e.PROTOCOL_NAME});return i.binaryType="arraybuffer",this.sdc=new a(i,this),this.sdc.onopen=function(e){t.sendHandover()},this.sdc.onclose=function(e){t.signaling.handoverState.any&&t.signaling.setState("closed")},this.sdc.onerror=function(e){console.error(t.logTag,"Signaling data channel error:",e)},this.sdc.onbufferedamountlow=function(e){console.warn(t.logTag,"Signaling data channel: Buffered amount low:",e)},this.sdc.onmessage=function(e){var n=new Uint8Array(e.data);t.signaling.onSignalingPeerMessage(n)},!0}},{key:"sendHandover",value:function(){console.debug(this.logTag,"Sending handover");try{this.signaling.sendTaskMessage({type:"handover"})}catch(e){"SignalingError"===e.name&&(console.error(this.logTag,"Could not send handover message",e.message),this.signaling.resetConnection(e.closeCode))}this.signaling.handoverState.local=!0,this.signaling.handoverState.both&&console.info(this.logTag,"Handover to data channel finished")}},{key:"wrapDataChannel",value:function(e){return console.debug(this.logTag,"Wrapping data channel",e.id),new a(e,this)}},{key:"close",value:function(e){console.debug(this.logTag,"Closing signaling data channel:",saltyrtcClient.explainCloseCode(e)),null!==this.sdc&&this.sdc.close(),this.sdc=null}},{key:"on",value:function(e,n){this.eventRegistry.register(e,n)}},{key:"once",value:function(e,n){var t=this,i=function e(i){try{n(i)}catch(n){throw t.off(i.type,e),n}t.off(i.type,e)};this.eventRegistry.register(e,i)}},{key:"off",value:function(e,n){this.eventRegistry.unregister(e,n)}},{key:"emit",value:function(e){console.debug(this.logTag,"New event:",e.type);var n=this.eventRegistry.get(e.type),t=!0,i=!1,r=void 0;try{for(var a,o=n[Symbol.iterator]();!(t=(a=o.next()).done);t=!0){var s=a.value;try{this.callHandler(s,e)}catch(n){console.error(this.logTag,"Unhandled exception in",e.type,"handler:",n)}}}catch(e){i=!0,r=e}finally{try{!t&&o.return&&o.return()}finally{if(i)throw r}}}},{key:"callHandler",value:function(e,n){var t=e(n);t===!1&&this.eventRegistry.unregister(n.type,e)}},{key:"logTag",get:function(){return null===this.signaling||void 0===this.signaling?"[SaltyRTC.WebRTC]":"[SaltyRTC.WebRTC."+this.signaling.role+"]"}}]),e}();o.PROTOCOL_NAME="v0.webrtc.tasks.saltyrtc.org",o.DEFAULT_MAX_PACKET_SIZE=16384,o.FIELD_EXCLUDE="exclude",o.FIELD_MAX_PACKET_SIZE="max_packet_size",o.FIELD_HANDOVER="handover",o.DC_LABEL="saltyrtc-signaling",o.CANDIDATE_BUFFERING_MS=5,e.WebRTCTask=o}(this.saltyrtcTaskWebrtc=this.saltyrtcTaskWebrtc||{});