UNPKG

@saltyrtc/task-webrtc

Version:
31 lines (30 loc) 14.4 kB
/** * saltyrtc-task-webrtc v0.15.0 * A SaltyRTC WebRTC task v1 implementation. * https://github.com/saltyrtc/saltyrtc-task-webrtc-js#readme * * Copyright (C) 2016-2022 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";var saltyrtcTaskWebrtc=function(e){class t{constructor(e,t,n,i){this.cookie=e,this.overflow=n,this.sequenceNumber=i,this.channelId=t}get combinedSequenceNumber(){return this.overflow*Math.pow(2,32)+this.sequenceNumber}static fromUint8Array(e){if(e.byteLength!==this.TOTAL_LENGTH)throw new saltyrtcClient.exceptions.ValidationError("Bad packet length");const n=new DataView(e.buffer,e.byteOffset,this.TOTAL_LENGTH),i=new Uint8Array(e.buffer,e.byteOffset,saltyrtcClient.Cookie.COOKIE_LENGTH),s=new saltyrtcClient.Cookie(i),a=n.getUint16(16),r=n.getUint16(18),o=n.getUint32(20);return new t(s,a,r,o)}toUint8Array(){const e=new Uint8Array(t.TOTAL_LENGTH);e.set(this.cookie.bytes);const n=new DataView(e.buffer,e.byteOffset,e.byteLength);return n.setUint16(16,this.channelId),n.setUint16(18,this.overflow),n.setUint32(20,this.sequenceNumber),e}}t.TOTAL_LENGTH=24;class n{constructor(e,t){this.lastIncomingCsn=null,this.channelId=e,this.signaling=t,this.cookiePair=new saltyrtcClient.CookiePair,this.csnPair=new saltyrtcClient.CombinedSequencePair}encrypt(e){const n=this.csnPair.ours.next(),i=new t(this.cookiePair.ours,this.channelId,n.overflow,n.sequenceNumber);return this.signaling.encryptForPeer(e,i.toUint8Array())}decrypt(e){let n;try{n=t.fromUint8Array(e.nonce)}catch(e){throw new saltyrtcClient.exceptions.ValidationError(`Unable to create nonce, reason: ${e}`)}if(n.cookie.equals(this.cookiePair.ours))throw new saltyrtcClient.exceptions.ValidationError("Local and remote cookie are equal");if(null===this.cookiePair.theirs||void 0===this.cookiePair.theirs)this.cookiePair.theirs=n.cookie;else if(!n.cookie.equals(this.cookiePair.theirs))throw new saltyrtcClient.exceptions.ValidationError("Remote cookie changed");if(null!==this.lastIncomingCsn&&n.combinedSequenceNumber===this.lastIncomingCsn)throw new saltyrtcClient.exceptions.ValidationError("CSN reuse detected");if(n.channelId!==this.channelId){const e="Data channel id in nonce does not match";throw new saltyrtcClient.exceptions.ValidationError(e)}return this.lastIncomingCsn=n.combinedSequenceNumber,this.signaling.decryptFromPeer(e)}}n.OVERHEAD_LENGTH=40,n.NONCE_LENGTH=t.TOTAL_LENGTH;class i{constructor(e,t){this.label="saltyrtc-signaling",this.id=e,this.protocol=t,this.untie()}untie(){this.closed=()=>{throw new Error("closed: Not tied to a SignalingTransport")},this.receive=()=>{throw new Error("receive: Not tied to a SignalingTransport")}}tie(e){this.closed=e.closed.bind(e),this.receive=e.receiveChunk.bind(e)}}class s{constructor(e,t,n,i,s,a,r){this.logTag="[SaltyRTC.WebRTC.SignalingTransport]",this.messageId=0,this.log=new saltyrtcClient.Log(a),this.link=e,this.handler=t,this.task=n,this.signaling=i,this.crypto=s,this.chunkLength=Math.min(this.handler.maxMessageSize,r),this.chunkBuffer=new ArrayBuffer(this.chunkLength),this.messageQueue=this.signaling.handoverState.peer?null:[],this.unchunker=new chunkedDc.UnreliableUnorderedUnchunker,this.unchunker.onMessage=this.receiveMessage.bind(this),this.link.tie(this),this.log.info(this.logTag,"Signaling transport created")}closed(){this.log.info("Closed (remote)"),this.unbind(),this.signaling.handoverState.any&&this.signaling.setState("closed")}receiveChunk(e){this.log.debug(this.logTag,"Received chunk");try{this.unchunker.add(e)}catch(e){return this.log.error(this.logTag,"Invalid chunk:",e),this.die()}}receiveMessage(e){this.log.debug(this.logTag,"Received message");const t=saltyrtcClient.Box.fromUint8Array(e,n.NONCE_LENGTH);try{e=this.crypto.decrypt(t)}catch(e){return this.log.error(this.logTag,"Invalid nonce:",e),this.die()}this.signaling.handoverState.peer?this.signaling.onSignalingPeerMessage(e):this.messageQueue.push(e)}flushMessageQueue(){if(!this.signaling.handoverState.peer)throw new Error("Remote did not request handover");for(const e of this.messageQueue)this.signaling.onSignalingPeerMessage(e);this.messageQueue=null}send(e){this.log.debug(this.logTag,"Sending message");e=this.crypto.encrypt(e).toUint8Array();const t=new chunkedDc.UnreliableUnorderedChunker(this.messageId++,e,this.chunkLength,this.chunkBuffer);for(let e of t){this.log.debug(this.logTag,"Sending chunk");try{this.handler.send(e)}catch(e){return this.log.error(this.logTag,"Unable to send chunk:",e),this.die()}}}close(){try{this.handler.close()}catch(e){this.log.error(this.logTag,"Unable to close data channel:",e)}this.log.info("Closed (local)"),this.unbind()}die(){this.log.warn(this.logTag,"Closing task due to an error"),this.task.close(saltyrtcClient.CloseCode.ProtocolError)}unbind(){this.link.untie(),this.unchunker.onMessage=void 0}}class a{constructor(e,t,n,i){this.logTag="[SaltyRTC.WebRTC]",this.initialized=!1,this.exclude=new Set,this.link=null,this.transport=null,this.eventRegistry=new saltyrtcClient.EventRegistry,this.candidates=[],this.sendCandidatesTimeout=null,this.version=e,this.log=new saltyrtcClient.Log(t),this.doHandover=n,this.maxChunkLength=i}set signaling(e){this._signaling=e,this.logTag="[SaltyRTC.WebRTC."+e.role+"]"}get signaling(){return this._signaling}init(e,t){this.processExcludeList(t[a.FIELD_EXCLUDE]),this.processHandover(t[a.FIELD_HANDOVER]),"v0"===this.version&&this.processMaxPacketSize(t[a.FIELD_MAX_PACKET_SIZE]),this.signaling=e,this.initialized=!0}processExcludeList(e){for(const t of e)this.exclude.add(t);for(let e=0;e<65535;e++)if(!this.exclude.has(e)){this.channelId=e;break}if(void 0===this.channelId&&this.doHandover)throw new Error("No free data channel id can be found")}processHandover(e){!1===e&&(this.doHandover=!1)}processMaxPacketSize(e){const t=this.maxChunkLength;if(!Number.isInteger(e))throw new RangeError(a.FIELD_MAX_PACKET_SIZE+" field must be an integer");if(e<0)throw new RangeError(a.FIELD_MAX_PACKET_SIZE+" field must be positive");e>0&&(this.maxChunkLength=Math.min(t,e)),this.log.debug(this.logTag,`Max packet size: Local requested ${t} bytes, remote requested ${e} bytes. Using ${this.maxChunkLength}.`)}onPeerHandshakeDone(){}onDisconnected(e){this.emit({type:"disconnected",data:e})}onTaskMessage(e){switch(this.log.debug(this.logTag,"New task message arrived: "+e.type),e.type){case"offer":if(!0!==this.validateOffer(e))return;this.emit({type:"offer",data:e.offer});break;case"answer":if(!0!==this.validateAnswer(e))return;this.emit({type:"answer",data:e.answer});break;case"candidates":if(!0!==this.validateCandidates(e))return;this.emit({type:"candidates",data:e.candidates});break;case"handover":if(!this.doHandover){this.log.error(this.logTag,"Received unexpected handover message from peer"),this.signaling.resetConnection(saltyrtcClient.CloseCode.ProtocolError);break}if(this.signaling.handoverState.peer){this.log.warn(this.logTag,"Handover already received");break}this.signaling.handoverState.peer=!0,null!==this.transport&&this.transport.flushMessageQueue(),this.signaling.handoverState.both&&this.log.info(this.logTag,"Handover to data channel finished");break;default:this.log.error(this.logTag,"Received message with unknown type:",e.type)}}validateOffer(e){return void 0===e.offer?(this.log.warn(this.logTag,"Offer message does not contain offer"),!1):void 0!==e.offer.sdp||(this.log.warn(this.logTag,"Offer message does not contain offer sdp"),!1)}validateAnswer(e){return void 0===e.answer?(this.log.warn(this.logTag,"Answer message does not contain answer"),!1):void 0!==e.answer.sdp||(this.log.warn(this.logTag,"Answer message does not contain answer sdp"),!1)}validateCandidates(e){if(void 0===e.candidates)return this.log.warn(this.logTag,"Candidates message does not contain candidates"),!1;if(e.candidates.length<1)return this.log.warn(this.logTag,"Candidates message contains empty candidate list"),!1;for(let t of e.candidates)if(null!==t){if("string"!=typeof t.candidate&&!(t.candidate instanceof String))return this.log.warn(this.logTag,"Candidates message contains invalid candidate (candidate field)"),!1;if("string"!=typeof t.sdpMid&&!(t.sdpMid instanceof String)&&null!==t.sdpMid)return this.log.warn(this.logTag,"Candidates message contains invalid candidate (sdpMid field)"),!1;if(null!==t.sdpMLineIndex&&!Number.isInteger(t.sdpMLineIndex))return this.log.warn(this.logTag,"Candidates message contains invalid candidate (sdpMLineIndex field)"),!1}return!0}sendSignalingMessage(e){if("task"!=this.signaling.getState())throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError,"Could not send signaling message: Signaling state is not 'task'.");if(!this.signaling.handoverState.local)throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError,"Could not send signaling message: Handover hasn't happened yet.");if(null===this.transport)throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError,"Could not send signaling message: Data channel is not established, yet.");this.transport.send(e)}getName(){return`${this.version}.webrtc.tasks.saltyrtc.org`}getSupportedMessageTypes(){return["offer","answer","candidates","handover"]}getData(){const e={};return e[a.FIELD_EXCLUDE]=Array.from(this.exclude.values()),e[a.FIELD_HANDOVER]=this.doHandover,"v0"===this.version&&(e[a.FIELD_MAX_PACKET_SIZE]=this.maxChunkLength),e}sendOffer(e){this.log.debug(this.logTag,"Sending offer");try{this.signaling.sendTaskMessage({type:"offer",offer:{type:e.type,sdp:e.sdp}})}catch(e){"SignalingError"===e.name&&(this.log.error(this.logTag,"Could not send offer:",e.message),this.signaling.resetConnection(e.closeCode))}}sendAnswer(e){this.log.debug(this.logTag,"Sending answer");try{this.signaling.sendTaskMessage({type:"answer",answer:{type:e.type,sdp:e.sdp}})}catch(e){"SignalingError"===e.name&&(this.log.error(this.logTag,"Could not send answer:",e.message),this.signaling.resetConnection(e.closeCode))}}sendCandidate(e){this.sendCandidates([e])}sendCandidates(e){this.log.debug(this.logTag,"Buffering",e.length,"candidate(s)"),this.candidates.push(...e);const t=()=>{try{this.log.debug(this.logTag,"Sending",this.candidates.length,"candidate(s)"),this.signaling.sendTaskMessage({type:"candidates",candidates:this.candidates})}catch(e){"SignalingError"===e.name&&(this.log.error(this.logTag,"Could not send candidates:",e.message),this.signaling.resetConnection(e.closeCode))}finally{this.candidates=[],this.sendCandidatesTimeout=null}};null===this.sendCandidatesTimeout&&(this.sendCandidatesTimeout=self.setTimeout(t,a.CANDIDATE_BUFFERING_MS))}getTransportLink(){if(this.log.debug(this.logTag,"Create signalling transport link"),!this.doHandover)throw new Error("Handover has not been negotiated");if(void 0===this.channelId){throw new Error("Data channel id not set")}return null===this.link&&(this.link=new i(this.channelId,this.getName())),this.link}handover(e){if(this.log.debug(this.logTag,"Initiate handover"),!this.doHandover)throw new Error("Handover has not been negotiated");if(this.signaling.handoverState.local||null!==this.transport)throw new Error("Handover already requested");const t=this.createCryptoContext(this.channelId);this.transport=new s(this.link,e,this,this.signaling,t,this.log.level,this.maxChunkLength),this.sendHandover()}sendHandover(){this.log.debug(this.logTag,"Sending handover");try{this.signaling.sendTaskMessage({type:"handover"})}catch(e){"SignalingError"===e.name&&(this.log.error(this.logTag,"Could not send handover message",e.message),this.signaling.resetConnection(e.closeCode))}this.signaling.handoverState.local=!0,this.signaling.handoverState.both&&this.log.info(this.logTag,"Handover to data channel finished")}createCryptoContext(e){return new n(e,this.signaling)}close(e){this.log.debug(this.logTag,"Closing signaling data channel:",saltyrtcClient.explainCloseCode(e)),null!==this.transport&&this.transport.close(),this.transport=null}on(e,t){this.eventRegistry.register(e,t)}once(e,t){const n=e=>{try{t(e)}catch(t){throw this.off(e.type,n),t}this.off(e.type,n)};this.eventRegistry.register(e,n)}off(e,t){void 0===e?this.eventRegistry.unregisterAll():this.eventRegistry.unregister(e,t)}emit(e){this.log.debug(this.logTag,"New event:",e.type);const t=this.eventRegistry.get(e.type);for(let n of t)try{this.callHandler(n,e)}catch(t){this.log.error(this.logTag,"Unhandled exception in",e.type,"handler:",t)}}callHandler(e,t){!1===e(t)&&this.eventRegistry.unregister(t.type,e)}}return a.FIELD_EXCLUDE="exclude",a.FIELD_HANDOVER="handover",a.FIELD_MAX_PACKET_SIZE="max_packet_size",a.CANDIDATE_BUFFERING_MS=5,e.DataChannelCryptoContext=n,e.WebRTCTaskBuilder=class{constructor(){this.version="v1",this.logLevel="none",this.handover=!0,this.maxChunkLength=262144}withLoggingLevel(e){return this.logLevel=e,this}withVersion(e){return this.version=e,this}withHandover(e){return this.handover=e,this}withMaxChunkLength(e){if(e<=chunkedDc.UNRELIABLE_UNORDERED_HEADER_LENGTH)throw new Error("Maximum chunk length must be greater than chunking overhead");return this.maxChunkLength=e,this}build(){return new a(this.version,this.logLevel,this.handover,this.maxChunkLength)}},Object.defineProperty(e,"__esModule",{value:!0}),e}({}); //# sourceMappingURL=saltyrtc-task-webrtc.es5.min.js.map