diff --git a/API.md b/API.md index e67f365..6ce0abd 100644 --- a/API.md +++ b/API.md @@ -1,543 +1,544 @@ ## API The entrypoint to the library is the `sylkrtc` object. Several objects (`Connection`, `Account` and `Call`) inherit from Node's `EventEmitter` class, you may want to check [its documentation](https://nodejs.org/api/events.html). ### sylkrtc The main entrypoint to the library. It exposes the main function to connect to SylkServer and some utility functions for general use. #### sylkrtc.createConnection(options={}) Creates a `sylkrtc` connection towards a SylkServer instance. The only supported option (at the moment) is "server", which should point to the WebSocket endpoint of the WebRTC gateway application. Example: `wss://1.2.3.4:8088/webrtcgateway/ws`. It returns a `Connection` object. Example: let connection = sylkrtc.createConnection({server: 'wss://1.2.3.4:8088/webrtcgateway/ws'}); #### sylkrtc.utils Helper module with utility functions. * `attachMediaStream`: function to easily attach a media stream to an element. It reexports [attachmediastream](https://github.com/otalk/attachMediaStream). * `closeMediaStream`: function to close the given media stream. +* `sanatizeHtml`: function to XSS sanitize html strings ### Connection Object representing the interaction with SylkServer. Multiple connections can be created with `sylkrtc.createConnection`, but typically only one is needed. Reconnecting in case the connection is interrupted is taken care of automatically. Events emitted: * **stateChanged**: indicates the WebSocket connection state has changed. Two arguments are provided: `oldState` and `newState`, the old connection state and the new connection state, respectively. Possible state values are: null, connecting, connected, ready, disconnected and closed. If the connection is involuntarily interrupted the state will transition to disconnected and the connection will be retried. Once the closed state is set, as a result of the user calling Connection.close(), the connection can no longer be used or reconnected. #### Connection.addAccount(options={}, cb=null) Configures an `Account` to be used through `sylkrtc`. 2 options are required: *account* (the account ID) and *password*. An optional *displayName* can be set. The account won't be registered, it will just be created. Optionally *realm* can be passed, which will be used instead of the domain for the HA1 calculation. The *password* won't be stored or transmitted as given, the HA1 hash (as used in [Digest access authentication](https://en.wikipedia.org/wiki/Digest_access_authentication)) is created and used instead. The `cb` argument is a callback which will be called with an error and the account object itself. Example: connection.addAccount({account: saghul@sip2sip.info, password: 1234}, function(error, account) { if (error) { console.log('Error adding account!' + account); } else { console.log('Account added!'); } }); #### Connection.removeAccount(account, cb=null) Removes the given account. The callback will be called once the operation completes (it cannot fail). The callback will be called with an error object. Example: connection.removeAccount(account, function(error) { console('Account removed!'); }); #### Connection.reconnect() Starts reconnecting immediately if the state was 'disconnected'; #### Connection.close() Close the connection with SylkServer. All accounts will be unbound. #### Connection.state Getter property returning the current connection state. ### Account Object representing a SIP account which will be used for making / receiving calls. Events emitted: * **registrationStateChanged**: indicates the SIP registration state has changed. Three arguments are provided: `oldState`, `newState` and `data`. `oldState` and `newState` represent the old registration state and the new registration state, respectively, and `data` is a generic per-state data object. Possible states: * null: registration hasn't started or it has ended * registering: registration is in progress * registered * failed: registration failed, the `data` object will contain a 'reason' property. * **outgoingCall**: emitted when an outgoing call is made. A single argument is provided: the `Call` object. * **incomingCall**: emitted when an incoming call is received. Two arguments are provided: the `Call` object and a `mediaTypes` object, which has 2 boolean properties: `audio` and `video`, indicating if those media types were present in the initial SDP. * **missedCall**: emitted when an incoming call is missed. A `data` object is provided, which contains an `originator` attribute, which is an `Identity` object. * **conferenceInvite**: emitted when someone invites us to join a conference. A `data` object is provided, which contains an `originator` attribute indicating who invited us, and a `room` attribute indicating what conference we have been invited to. #### Account.register() Start the SIP registration process for the account. Progress will be reported via the *registrationStateChanged* event. Note: it's not necessary to be registered to make an outgoing call. #### Account.unregister() Unregister the account. Progress will be reported via the *registrationStateChanged* event. #### Account.call(uri, options={}) Start an outgoing call. Supported options: * pcConfig: configuration options for `RTCPeerConnection`. [Reference](http://w3c.github.io/webrtc-pc/#configuration). * offerOptions: `RTCOfferOptions`. [Reference](http://w3c.github.io/webrtc-pc/#idl-def-RTCOfferOptions). * localStream: user provided local media stream (acquired with `getUserMedia` TODO). Example: const call = account.call('3333@sip2sip.info', {localStream: stream}); #### Account.joinConference(uri, options={}) Join (or create in case it doesn't exist) a multi-party video conference at the given URI. Supported options: * pcConfig: configuration options for `RTCPeerConnection`. [Reference](http://w3c.github.io/webrtc-pc/#configuration). * offerOptions: `RTCOfferOptions`. [Reference](http://w3c.github.io/webrtc-pc/#idl-def-RTCOfferOptions). * localStream: user provided local media stream (acquired with `getUserMedia` TODO). Example: const conf = account.joinConference('test123@conference.sip2sip.info', {localStream: stream}); #### Account.id Getter property returning the account ID. #### Account.displayName Getter property returning the account display name. #### Account.password Getter property returning the HA1 password for the account. #### Account.registrationState Getter property returning the current registration state. #### Account.setDeviceToken(oldToken, newToken) Set the current device token for this account. The device token is an opaque string usually provided by the Firebase SDK which SylkServer can use to send push notifications. ### Call Object representing a audio/video call. Signalling is done using SIP underneath. Events emitted: * **localStreamAdded**: emitted when the local stream is added to the call. A single argument is provided: the stream itself. * **streamAdded**: emitted when a remote stream is added to the call. A single argument is provided: the stream itself. * **stateChanged**: indicates the call state has changed. Three arguments are provided: `oldState`, `newState` and `data`. `oldState` and `newState` indicate the previous and current state respectively, and `data` is a generic per-state data object. Possible states: * terminated: the call has ended (the `data` object contains a `reason` attribute) * accepted: the call has been accepted (either locally or remotely) * incoming: initial state for incoming calls * progress: initial state for outgoing calls * established: call media has been established * **dtmfToneSent**: emitted when one of the tones passed to `sendDtmf` is actually sent. An empty tone indicates all tones have finished playing. #### Call.answer(options={}) Answer an incoming call. Supported options: * pcConfig: configuration options for `RTCPeerConnection`. [Reference](http://w3c.github.io/webrtc-pc/#configuration). * answerOptions: `RTCAnswerOptions`. [Reference](http://w3c.github.io/webrtc-pc/#idl-def-RTCAnswerOptions). * localStream: user provided local media stream (acquired with `getUserMedia` TODO). #### Call.startScreensharing(newTrack) Start sharing a screen/window. `newTrack` should be a `RTCMediaStreamTrack` containing the screen/window. Internally it will call replace track with the keep flag enabled and it will set the state so it can be tracked. #### Call.stopScreensharing() Stop sharing a screen/window and restore the previousTrack. #### Call.replaceTrack(oldTrack, newTrack, keep=false, cb=null) Replace a local track inside a call. If the keep flag is set, it will store the replaced track internally so it can be used later. The callback will be called with a true value once the operation completes. #### Call.terminate() End the call. #### Call.getLocalStreams() Returns an array of *local* `RTCMediaStream` objects. #### Call.getRemoteStreams() Returns an array of *remote* `RTCMediaStream` objects. #### Call.getSenders() Returns an array of `RTCRtpSender` objects. #### Call.getReceivers() Returns an array of `RTCRtpReceiver` objects. #### Call.sendDtmf(tones, duration=100, interToneGap=70) Sends the given DTMF tones over the active audio stream track. **Note**: This feature requires browser support for `RTCPeerConnection.createDTMFSender`. #### Call.account Getter property which returns the `Account` object associated with this call. #### Call.id Getter property which returns the ID for this call. Note: this is not related to the SIP Call-ID header. #### Call.sharingScreen Getter property which returns the screen sharing state. #### Call.direction Getter property which returns the call direction: "incoming" or "outgoing". Note: this is not related to the SDP "a=" direction attribute. #### Call.state Getter property which returns the call state. #### Call.localIdentity Getter property which returns the local identity. (See the `Identity` object). #### Call.remoteIdentity Getter property which returns the remote identity. (See the `Identity` object). #### Call.remoteMediaDirections Getter property which returns an object with the directions of the remote streams. Note: this **is** related to the SDP "a=" direction attribute. ### Conference Object representing a multi-party audio/video conference. Events emitted: * **localStreamAdded**: emitted when the local stream is added to the call. A single argument is provided: the stream itself. * **stateChanged**: indicates the conference state has changed. Three arguments are provided: `oldState`, `newState` and `data`. `oldState` and `newState` indicate the previous and current state respectively, and `data` is a generic per-state data object. Possible states: * terminated: the conference has ended * accepted: the initial offer has been accepted * progress: initial state * established: conference has been established and media is flowing * **participantJoined**: emitted when a participant joined the conference. A single argument is provided: an instance of `Participant`. Note that this event is only emitted when new participants join, `Conference.participants` should be checked upon the initial join to check what participants are already in the conference. * **participantLeft**: emitted when a participant leaves the conference. A single argument is provided: an instance of `Participant`. * **roomConfigured**: emitted when the room is configured by the server. A single argument is provided: an object with the `originator` of the message which is an `Identity` or string and a list of `activeParticipants`. The list contains instances of `Participant`. * **fileSharing**: emitted when a participant in the room shares files. A single argument is provided: a list of instances of `SharedFile`. #### Conference.startScreensharing(newTrack) Start sharing a screen/window. `newTrack` should be a `RTCMediaStreamTrack` containing the screen/window. Internally it will call replace track with the keep flag enabled and it will set the state so it can be tracked. #### Conference.stopScreensharing() Stop sharing a screen/window and restore the previousTrack. #### Conference.replaceTrack(oldTrack, newTrack, keep=false, cb=null) Replace a local track inside the conference. If the keep flag is set, it will store the replaced track internally so it can be used later. The callback will be called with a true value once the operation completes. #### Conference.getLocalStreams() Returns an array of *local* `RTCMediaStream` objects. These are the streams being published to the conference. #### Conference.getRemoteStreams() Returns an array of *remote* `RTCMediaStream` objects. These are the streams published by all other participants in the conference. #### Conference.getSenders() Returns an array of `RTCRtpSender` objects. The sender objects get the *local* tracks being published to the conference. #### Conference.getReceivers() Returns an array of `RTCRtpReceiver` objects. The receiver objects get the *remote* tracks published by all other participants in the conference. #### Conference.scaleLocalTrack(track, divider) Scale the given local video track by a given divider. Currently this function will not work, since browser support is lacking. #### Conference.configureRoom(participants, cb=null) Configure the room. `Participants` is a list with the publisher session ids of the new active participants. The active participants will get more bandwidth and the other participants will get a limited bandwidth. On success the *roomConfigured* event is emitted. The `cb` argument is a callback which will be called on an error with error as argument. #### Conference.participants Getter property which returns an array of `Participant` objects in the conference. #### Conference.activeParticipants Getter property for the Active Participants which returns an array of `Participant` objects in the conference. #### Conference.sharedFiles Getter property for the Shared Files which returns an array of `SharedFile` objects in the conference. #### Conference.account Getter property which returns the `Account` object associated with this conference. #### Conference.id Getter property which returns the ID for this conference. Note: this is not related to the URI. #### Conference.sharingScreen Getter property which returns the screen sharing state. #### Conference.direction Dummy property always returning "outgoing", in order to provide the same API as `Call`. #### Conference.state Getter property which returns the conference state. #### Conference.localIdentity Getter property which returns the local identity. (See the `Identity` object). This will always be built from the account. #### Conference.remoteIdentity Getter property which returns the remote identity. (See the `Identity` object). This will always be built from the remote URI. ### Participant Object representing another user connected to the same conference. Events emitted: * **streamAdded**: emitted when a remote stream is added. A single argument is provided: the stream itself. * **stateChanged**: indicates the participant state has changed. Three arguments are provided: `oldState`, `newState` and `data`. `oldState` and `newState` indicate the previous and current state respectively, and `data` is a generic per-state data object. Possible states: * null: initial state * progress: the participant is being attached to, this will happen as a result to `Participant.attach` * established: media is flowing from this participant #### Participant.id Getter property which returns the ID for this participant. Note this an abstract ID. #### Participant.state Getter property which returns the participant state. #### Participant.identity Getter property which returns the participant's identity. (See the `Identity` object). #### Participant.publisherId Getter property which returns the participant's publisher session id. #### Participant.streams Getter property which returns the audio / video streams for this participant. #### Participant.videoPaused Getter property which returns true / false when the video subscription is paused / not paused #### Participant.getReceivers() Returns an array of `RTCRtpReceiver` objects. The receiver objects get the *remote* tracks published by the participant. #### Participant.attach() Start receiving audio / video from this participant. Once attached the participant's state will switch to 'established' and its audio /video stream(s) will be available in `Participant.streams`. If a participant is not attached to, no audio or video will be received from them. #### Participant.detach(isRemoved=false) Stop receiving audio / video from this participant. The opposite of `Participant.attach()`. The isRemoved option needs to be true used when the participant has already left. This is the case when you receive the 'participantLeft' event. #### Participant.pauseVideo() Stop receiving video from this participant. The opposite of `Participant.resumeVideo()`. #### Participant.resumeVideo() Resume receiving video from this participant. The opposite of `Participant.pauseVideo()`. ### Identity Object representing the identity of the caller / callee. #### Identity.uri SIP URI, without the 'sip:' prefix. #### Identity.displayName Display name assiciated with the identity. Set to '' if absent. #### Identity.toString() Function returning a string representation of the identity. It can take 2 forms depending on the availability of the display name: 'bob@biloxi.com' or 'Bob '. ### SharedFile Object representing a shared file. #### SharedFile.filename The filename of the shared file #### SharedFile.filesize The filesize in bytes of the shared file #### SharedFile.uploader The `Identity` of the uploader. #### SharedFile.session The session UUID which was used to upload the file diff --git a/lib/utils.js b/lib/utils.js index 4e71b10..1fe1c2a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,174 +1,177 @@ 'use strict'; import transform from 'sdp-transform'; import attachMediaStream from 'attachmediastream'; - +import DOMPurify from 'dompurify' class Identity { constructor(uri, displayName=null) { this._uri = uri; this._displayName = displayName; } get uri() { return this._uri; } get displayName() { return this._displayName; } toString() { if (!this._displayName) { return this._uri; } else { return `${this._displayName} <${this._uri}>`; } } } class SharedFile { constructor(filename, filesize, uploader, session) { this._filename = filename; this._filesize = filesize; this._uploader = uploader; this._session = session; } get filename() { return this._filename; } get filesize() { return this._filesize; } get uploader() { return this._uploader; } get session() { return this._session; } } function createLocalSdp(pc, type, options) { if (type !== 'offer' && type !== 'answer') { return Promise.reject('type must be "offer" or "answer", but "' +type+ '" was given'); } let p = new Promise(function(resolve, reject) { let createFunc; if (type === 'offer' ) { createFunc = pc.createOffer; } else { createFunc = pc.createAnswer; } createFunc.call( pc, // success function(desc) { pc.setLocalDescription( desc, // success function() { resolve(mungeSdp(pc.localDescription.sdp)); }, // failure function(error) { reject('Error setting local description: ' + error); } ); }, // failure function(error) { reject('Error creating local SDP: ' + error); }, options ); }); return p; } function mungeSdp(sdp) { let parsedSdp = transform.parse(sdp); let h264payload = null; let hasProfileLevelId = false; // try to fix H264 support for (let media of parsedSdp.media) { if (media.type === 'video') { for (let rtp of media.rtp) { if (rtp.codec === 'H264') { h264payload = rtp.payload; break; } } if (h264payload !== null) { for (let fmtp of media.fmtp) { if (fmtp.payload === h264payload && fmtp.config.indexOf('profile-level-id') !== -1) { hasProfileLevelId = true; break; } } if (!hasProfileLevelId) { media.fmtp.push({payload: h264payload, config: 'profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1'}); } break; } } } // remove bogus rtcp-fb elements for (let media of parsedSdp.media) { let payloads = String(media.payloads).split(' '); if (media.rtcpFb) { media.rtcpFb = media.rtcpFb.filter((item) => { return payloads.indexOf(String(item.payload)) !== -1; }); } } return transform.write(parsedSdp); } function getMediaDirections(sdp) { const parsedSdp = transform.parse(sdp); const directions = {}; for (let media of parsedSdp.media) { directions[media.type] = (directions[media.type] || []).concat(media.direction); } return directions; } function closeMediaStream(stream) { if (!stream) { return; } // Latest spec states that MediaStream has no stop() method and instead must // call stop() on every MediaStreamTrack. if (MediaStreamTrack && MediaStreamTrack.prototype && MediaStreamTrack.prototype.stop) { if (stream.getTracks) { for (let track of stream.getTracks()) { track.stop(); } } else { for (let track of stream.getAudioTracks()) { track.stop(); } for (let track of stream.getVideoTracks()) { track.stop(); } } // Deprecated by the spec, but still in use. } else if (typeof stream.stop === 'function') { stream.stop(); } } +function sanatizeHtml(html) { + return DOMPurify.sanitize(html.trim()); +} -export default { Identity, SharedFile, createLocalSdp, mungeSdp, getMediaDirections, attachMediaStream, closeMediaStream}; +export default { Identity, SharedFile, createLocalSdp, mungeSdp, getMediaDirections, attachMediaStream, closeMediaStream, sanatizeHtml}; diff --git a/package.json b/package.json index d476239..f878b24 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,52 @@ { "name": "sylkrtc", "version": "1.1.1", "main": "lib/sylkrtc.js", "description": "SylkServer WebRTC Gateway client library", "repository": { "type": "git", "url": "git://github.com/AGProjects/sylkrtc.git" }, "keywords": [], "author": "AG Projects", "contributors": [ "Tijmen de Mes ", "Saúl Ibarra Corretgé " ], "license": "MIT", "readmeFilename": "README.md", "browserify": { "transform": [ "babelify" ] }, "dependencies": { "attachmediastream": "^2.0.0", "blueimp-md5": "^2.10.0", "bowser": "^2.7.0", "debug": "^2.6.8", + "dompurify": "^2.0.7", "sdp-transform": "^2.3.0", "uuid": "^3.1.0", "webrtc-adapter": "4.1.1", "websocket": "^1.0.28" }, "devDependencies": { "babel-core": "^6.26.3", "babel-preset-es2015": "^6.9.0", "babelify": "^7.3.0", "browserify": "^16.5.0", "gulp": "^4.0.2", "gulp-filelog": "^0.4.1", "gulp-header": "^2.0.9", "gulp-jshint": "^2.0.1", "gulp-sourcemaps": "^2.6.1", "gulp-uglify": "^3.0.0", "jshint": "^2.9.5", "jshint-stylish": "^2.2.0", "minimist": "^1.2.0", "through2": "^3.0.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^2.0.0" } }