ringcentral-softphone
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -20,10 +20,12 @@ "use strict"; | ||
var _RequestSipMessage = _interopRequireDefault(require("./SipMessage/outbound/RequestSipMessage")); | ||
var _requestSipMessage = _interopRequireDefault(require("./sip-message/outbound/request-sip-message")); | ||
var _InboundSipMessage = _interopRequireDefault(require("./SipMessage/inbound/InboundSipMessage")); | ||
var _inboundSipMessage = _interopRequireDefault(require("./sip-message/inbound/inbound-sip-message")); | ||
var _ResponseSipMessage = _interopRequireDefault(require("./SipMessage/outbound/ResponseSipMessage")); | ||
var _responseSipMessage = _interopRequireDefault(require("./sip-message/outbound/response-sip-message")); | ||
var _utils = require("./utils"); | ||
var _rcMessage = _interopRequireDefault(require("./rc-message/rc-message")); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -37,27 +39,50 @@ | ||
this.fakeEmail = (0, _v.default)() + '@' + this.fakeDomain; | ||
this.branch = () => 'z9hG4bK' + (0, _v.default)(); | ||
this.fromTag = (0, _v.default)(); | ||
this.callerId = (0, _v.default)(); | ||
this.callId = (0, _v.default)(); | ||
} | ||
newCallId() { | ||
this.callId = (0, _v.default)(); | ||
} | ||
async handleSipMessage(inboundSipMessage) { | ||
if (inboundSipMessage.subject.startsWith('INVITE sip:')) { | ||
// invite | ||
await this.send(new _ResponseSipMessage.default(inboundSipMessage, 100, 'Trying')); | ||
await this.send(new _ResponseSipMessage.default(inboundSipMessage, 180, 'Ringing', { | ||
await this.response(inboundSipMessage, 180, { | ||
Contact: `<sip:${this.fakeDomain};transport=ws>` | ||
})); | ||
this.inviteSipMessage = inboundSipMessage; | ||
this.emit('INVITE', this.inviteSipMessage); | ||
}); | ||
await this.sendRcMessage(inboundSipMessage, 17); | ||
this.emit('INVITE', inboundSipMessage); | ||
} else if (inboundSipMessage.subject.startsWith('BYE ')) { | ||
// bye | ||
await this.response(inboundSipMessage, 200); | ||
this.emit('BYE', inboundSipMessage); | ||
} else if (inboundSipMessage.subject.startsWith('MESSAGE ') && inboundSipMessage.body.includes(' Cmd="7"')) { | ||
// take over | ||
await this.send(new _ResponseSipMessage.default(inboundSipMessage, 200, 'OK')); | ||
// server side: already processed | ||
await this.response(inboundSipMessage, 200); | ||
} | ||
} | ||
async sendRcMessage(inboundSipMessage, reqid) { | ||
const rcMessage = _rcMessage.default.fromXml(inboundSipMessage.headers['P-rc']); | ||
const newRcMessage = new _rcMessage.default({ | ||
SID: rcMessage.Hdr.SID, | ||
Req: rcMessage.Hdr.Req, | ||
From: rcMessage.Hdr.To, | ||
To: rcMessage.Hdr.From, | ||
Cmd: reqid | ||
}, { | ||
Cln: this.sipInfo.authorizationId | ||
}); | ||
const requestSipMessage = new _requestSipMessage.default(`MESSAGE sip:${newRcMessage.Hdr.To} SIP/2.0`, { | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${(0, _utils.branch)()}`, | ||
To: `<sip:${newRcMessage.Hdr.To}>`, | ||
From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${this.fromTag}`, | ||
'Call-ID': this.callId, | ||
'Content-Type': 'x-rc/agent' | ||
}, newRcMessage.toXml()); | ||
await this.send(requestSipMessage); | ||
} | ||
async send(sipMessage) { | ||
@@ -77,3 +102,3 @@ return new Promise((resolve, reject) => { | ||
if (inboundSipMessage.subject === 'SIP/2.0 100 Trying') { | ||
if (inboundSipMessage.subject === 'SIP/2.0 100 Trying' || inboundSipMessage.subject === 'SIP/2.0 183 Session Progress') { | ||
return; // ignore | ||
@@ -84,3 +109,3 @@ } | ||
if (inboundSipMessage.subject.startsWith('SIP/2.0 603 ')) { | ||
if (inboundSipMessage.subject.startsWith('SIP/2.0 5') || inboundSipMessage.subject.startsWith('SIP/2.0 6') || inboundSipMessage.subject.startsWith('SIP/2.0 403')) { | ||
reject(inboundSipMessage); | ||
@@ -98,2 +123,6 @@ return; | ||
async response(inboundSipMessage, responseCode, headers = {}, body = '') { | ||
await this.send(new _responseSipMessage.default(inboundSipMessage, responseCode, headers, body)); | ||
} | ||
async register() { | ||
@@ -130,3 +159,3 @@ const r = await this.rc.post('/restapi/v1.0/client-info/sip-provision', { | ||
this.ws.addEventListener('message', e => { | ||
const sipMessage = _InboundSipMessage.default.fromString(e.data); | ||
const sipMessage = _inboundSipMessage.default.fromString(e.data); | ||
@@ -139,8 +168,10 @@ this.emit('sipMessage', sipMessage); | ||
this.ws.removeEventListener('open', openHandler); | ||
const requestSipMessage = new _RequestSipMessage.default(`REGISTER sip:${this.sipInfo.domain} SIP/2.0`, { | ||
'Call-ID': this.callerId, | ||
const requestSipMessage = new _requestSipMessage.default(`REGISTER sip:${this.sipInfo.domain} SIP/2.0`, { | ||
'Call-ID': this.callId, | ||
Contact: `<sip:${this.fakeEmail};transport=ws>;expires=600`, | ||
From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${this.fromTag}`, | ||
To: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>`, | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${this.branch()}` | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${(0, _utils.branch)()}`, | ||
Allow: 'ACK,CANCEL,INVITE,MESSAGE,BYE,OPTIONS,INFO,NOTIFY,REFER', | ||
Supported: 'path, gruu, outbound' | ||
}); | ||
@@ -153,4 +184,9 @@ let inboundSipMessage = await this.send(requestSipMessage); | ||
const nonce = wwwAuth.match(/, nonce="(.+?)"/)[1]; | ||
requestSipMessage.headers.Authorization = (0, _utils.generateAuthorization)(this.sipInfo, 'REGISTER', nonce); | ||
inboundSipMessage = await this.send(requestSipMessage); | ||
const newRequestSipMessage = requestSipMessage.fork(); | ||
newRequestSipMessage.headers.Authorization = (0, _utils.generateAuthorization)(this.sipInfo, 'REGISTER', nonce); | ||
inboundSipMessage = await this.send(newRequestSipMessage); | ||
if (inboundSipMessage.subject === 'SIP/2.0 200 OK') { | ||
this.emit('registered'); | ||
} | ||
} | ||
@@ -162,4 +198,4 @@ }; | ||
async answer(inputAudioStream = undefined) { | ||
const sdp = this.inviteSipMessage.body; | ||
async answer(inviteSipMessage, inputAudioStream = undefined) { | ||
const sdp = inviteSipMessage.body; | ||
const remoteRtcSd = new _isomorphicWebrtc.RTCSessionDescription({ | ||
@@ -186,10 +222,89 @@ type: 'offer', | ||
peerConnection.setLocalDescription(localRtcSd); | ||
await this.send(new _ResponseSipMessage.default(this.inviteSipMessage, 200, 'OK', { | ||
await this.response(inviteSipMessage, 200, { | ||
Contact: `<sip:${this.fakeEmail};transport=ws>`, | ||
'Content-Type': 'application/sdp' | ||
}, localRtcSd.sdp)); | ||
}, localRtcSd.sdp); | ||
} | ||
async reject() {} | ||
async toVoicemail(inviteSipMessage) { | ||
await this.sendRcMessage(inviteSipMessage, 11); | ||
} | ||
async ignore(inviteSipMessage) { | ||
await this.response(inviteSipMessage, 480); | ||
} | ||
async invite(callee, inputAudioStream = undefined) { | ||
this.newCallId(); | ||
const peerConnection = new _isomorphicWebrtc.RTCPeerConnection({ | ||
iceServers: [{ | ||
urls: 'stun:74.125.194.127:19302' | ||
}] | ||
}); | ||
/* this is for debugging - start */ | ||
// const eventNames = [ | ||
// 'addstream', 'connectionstatechange', 'datachannel', 'icecandidate', | ||
// 'iceconnectionstatechange', 'icegatheringstatechange', 'identityresult', | ||
// 'negotiationneeded', 'removestream', 'signalingstatechange', 'track' | ||
// ] | ||
// for (const eventName of eventNames) { | ||
// peerConnection.addEventListener(eventName, e => { | ||
// console.log(`\n****** RTCPeerConnection ${eventName} event - start *****`) | ||
// console.log(e) | ||
// console.log(`****** RTCPeerConnection ${eventName} event - end *****\n`) | ||
// }) | ||
// } | ||
/* this is for debugging - end */ | ||
if (inputAudioStream) { | ||
const track = inputAudioStream.getAudioTracks()[0]; | ||
peerConnection.addTrack(track, inputAudioStream); | ||
} | ||
const localRtcSd = await peerConnection.createOffer(); | ||
peerConnection.setLocalDescription(localRtcSd); | ||
const requestSipMessage = new _requestSipMessage.default(`INVITE sip:${callee}@${this.sipInfo.domain} SIP/2.0`, { | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${(0, _utils.branch)()}`, | ||
To: `<sip:${callee}@${this.sipInfo.domain}>`, | ||
From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${this.fromTag}`, | ||
'Call-ID': this.callId, | ||
Contact: `<sip:${this.fakeEmail};transport=ws;ob>`, | ||
'Content-Type': 'application/sdp', | ||
// 'P-rc-country-id': 1, | ||
// 'P-rc-endpoint-id': uuid(), | ||
// 'Client-id': process.env.RINGCENTRAL_CLIENT_ID, | ||
// 'P-Asserted-Identity': 'sip:+16504223279@sip.ringcentral.com', | ||
Allow: 'ACK,CANCEL,INVITE,MESSAGE,BYE,OPTIONS,INFO,NOTIFY,REFER', | ||
Supported: 'outbound' | ||
}, localRtcSd.sdp); | ||
let inboundSipMessage = await this.send(requestSipMessage); | ||
const wwwAuth = inboundSipMessage.headers['Proxy-Authenticate']; | ||
if (wwwAuth && wwwAuth.includes(', nonce="')) { | ||
// authorization required | ||
const ackMessage = new _requestSipMessage.default(`ACK ${inboundSipMessage.headers.Contact.match(/<(.+?)>/)[1]} SIP/2.0`, { | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${(0, _utils.branch)()}`, | ||
To: inboundSipMessage.headers.To, | ||
From: inboundSipMessage.headers.From, | ||
'Call-ID': this.callId, | ||
Supported: 'outbound' | ||
}); | ||
ackMessage.reuseCseq(); | ||
this.send(ackMessage); | ||
const nonce = wwwAuth.match(/, nonce="(.+?)"/)[1]; | ||
const newRequestSipMessage = requestSipMessage.fork(); | ||
newRequestSipMessage.headers['Proxy-Authorization'] = (0, _utils.generateProxyAuthorization)(this.sipInfo, 'INVITE', callee, nonce); | ||
inboundSipMessage = await this.send(newRequestSipMessage); | ||
const remoteRtcSd = new _isomorphicWebrtc.RTCSessionDescription({ | ||
type: 'answer', | ||
sdp: inboundSipMessage.body | ||
}); | ||
peerConnection.addEventListener('track', e => { | ||
this.emit('track', e); | ||
}); | ||
peerConnection.setRemoteDescription(remoteRtcSd); | ||
} | ||
} | ||
} | ||
@@ -196,0 +311,0 @@ |
@@ -6,6 +6,8 @@ "use strict"; | ||
}); | ||
exports.generateProxyAuthorization = exports.generateAuthorization = void 0; | ||
exports.branch = exports.generateProxyAuthorization = exports.generateAuthorization = void 0; | ||
var _blueimpMd = _interopRequireDefault(require("blueimp-md5")); | ||
var _v = _interopRequireDefault(require("uuid/v4")); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -54,2 +56,6 @@ | ||
exports.generateProxyAuthorization = generateProxyAuthorization; | ||
const branch = () => 'z9hG4bK' + (0, _v.default)(); | ||
exports.branch = branch; | ||
//# sourceMappingURL=utils.js.map |
{ | ||
"name": "ringcentral-softphone", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"license": "MIT", | ||
"main": "dist/index.js", | ||
"scripts": { | ||
"server": "node -r dotenv-override-true/config -r @babel/register demos/node.js", | ||
"server": "node -r dotenv-override-true/config -r @babel/register demos/node/answer-and-talk.js", | ||
"browser": "webpack-dev-server --progress --colors --open", | ||
"test": "standard && jest", | ||
"release": "babel src --out-dir dist --source-maps", | ||
"prepush": "yarn test", | ||
"prepublishOnly": "yarn release" | ||
@@ -22,6 +21,6 @@ }, | ||
"devDependencies": { | ||
"@babel/cli": "^7.6.0", | ||
"@babel/core": "^7.6.0", | ||
"@babel/preset-env": "^7.6.0", | ||
"@babel/register": "^7.6.0", | ||
"@babel/cli": "^7.6.2", | ||
"@babel/core": "^7.6.2", | ||
"@babel/preset-env": "^7.6.2", | ||
"@babel/register": "^7.6.2", | ||
"@ringcentral/sdk": "^4.0.0-alpha.7", | ||
@@ -37,3 +36,3 @@ "core-js": "^3.2.1", | ||
"standard": "^14.3.1", | ||
"webpack": "^4.40.2", | ||
"webpack": "^4.41.0", | ||
"webpack-cli": "^3.3.9", | ||
@@ -47,3 +46,8 @@ "webpack-dev-server": "^3.8.1", | ||
"@ringcentral/sdk": "^4.0.0-alpha.7" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-push": "yarn test" | ||
} | ||
} | ||
} |
@@ -7,5 +7,27 @@ # RingCentral Softphone SDK for JavaScript | ||
This project was originally designed for server and desktop. It works both with and without browsers. | ||
This project was originally designed for server and desktop. It doesn't require a browser to run. It could run in browser too. | ||
## Supported features: | ||
- Answer inbound call | ||
- Make outbound call | ||
- Speak and listen, two way communication | ||
- Call control features | ||
- Redirect inbound call to voicemail | ||
- Ignore inbound call | ||
## Demos | ||
- [browser demo](./demos/browser) | ||
- node.js | ||
- [answer inbound call](./demos/node/answer-and-talk.js) | ||
- [make outbound call](./demos/node/outbound-call.js) | ||
- [redirect inbound call to voicemail](./demos/node/to-voicemail.js) | ||
- [ignore inbound call](./demos/node/ignore.js) | ||
- [call supervise](https://github.com/tylerlong/ringcentral-call-supervise-demo) | ||
- supervise an existing phone call and get real time audio stream | ||
## Install | ||
@@ -23,12 +45,10 @@ | ||
because node.js by default doesn't support WebSocket & WebRTC. | ||
## Usage | ||
- for node.js, check [this demo](./demos/node.js) | ||
- for browser, check [this demo](./demos/browser.js) | ||
- for node.js, check [here](./demos/node) | ||
- for browser, check [here](./demos/browser) | ||
## Demos | ||
## Official demos | ||
@@ -44,3 +64,5 @@ ### Setup | ||
- `CALLEE_FOR_TESTING` is a phone number to receive testing phone calls. You don't need to specify it if you do not make outbound calls. | ||
### Run | ||
@@ -54,8 +76,5 @@ | ||
Make a phone call to the phone number you configured in `.env` file. | ||
Make a phone call to the phone number you configured in `.env` file. The demo app will answer the call and you can speak and listen. | ||
- for node.js, the app will auto pick up the call and redirect your voice to speaker. | ||
- for browser, the app will auto pick up the call and redirect your voice to an `<audio/>` HTML5 element. | ||
## Interesting Usage cases | ||
@@ -81,4 +100,11 @@ | ||
## Play recorded audio | ||
You can create a program to make a phone cal or answer a phone call and play recorded audio. This is good for announcement purpose. This is also good for quick voicemail drop. | ||
## Todo | ||
- make outbound call | ||
- stay alive | ||
- How to create a publish message | ||
- How to forward a call |
156
src/index.js
@@ -6,6 +6,7 @@ import uuid from 'uuid/v4' | ||
import RequestSipMessage from './SipMessage/outbound/RequestSipMessage' | ||
import InboundSipMessage from './SipMessage/inbound/InboundSipMessage' | ||
import ResponseSipMessage from './SipMessage/outbound/ResponseSipMessage' | ||
import { generateAuthorization } from './utils' | ||
import RequestSipMessage from './sip-message/outbound/request-sip-message' | ||
import InboundSipMessage from './sip-message/inbound/inbound-sip-message' | ||
import ResponseSipMessage from './sip-message/outbound/response-sip-message' | ||
import { generateAuthorization, generateProxyAuthorization, branch } from './utils' | ||
import RcMessage from './rc-message/rc-message' | ||
@@ -18,22 +19,49 @@ class Softphone extends EventEmitter { | ||
this.fakeEmail = uuid() + '@' + this.fakeDomain | ||
this.branch = () => 'z9hG4bK' + uuid() | ||
this.fromTag = uuid() | ||
this.callerId = uuid() | ||
this.callId = uuid() | ||
} | ||
newCallId () { | ||
this.callId = uuid() | ||
} | ||
async handleSipMessage (inboundSipMessage) { | ||
if (inboundSipMessage.subject.startsWith('INVITE sip:')) { // invite | ||
await this.send(new ResponseSipMessage(inboundSipMessage, 100, 'Trying')) | ||
await this.send(new ResponseSipMessage(inboundSipMessage, 180, 'Ringing', { | ||
await this.response(inboundSipMessage, 180, { | ||
Contact: `<sip:${this.fakeDomain};transport=ws>` | ||
})) | ||
this.inviteSipMessage = inboundSipMessage | ||
this.emit('INVITE', this.inviteSipMessage) | ||
}) | ||
await this.sendRcMessage(inboundSipMessage, 17) | ||
this.emit('INVITE', inboundSipMessage) | ||
} else if (inboundSipMessage.subject.startsWith('BYE ')) { // bye | ||
await this.response(inboundSipMessage, 200) | ||
this.emit('BYE', inboundSipMessage) | ||
} else if (inboundSipMessage.subject.startsWith('MESSAGE ') && inboundSipMessage.body.includes(' Cmd="7"')) { // take over | ||
await this.send(new ResponseSipMessage(inboundSipMessage, 200, 'OK')) | ||
} else if (inboundSipMessage.subject.startsWith('MESSAGE ') && inboundSipMessage.body.includes(' Cmd="7"')) { // server side: already processed | ||
await this.response(inboundSipMessage, 200) | ||
} | ||
} | ||
async sendRcMessage (inboundSipMessage, reqid) { | ||
const rcMessage = RcMessage.fromXml(inboundSipMessage.headers['P-rc']) | ||
const newRcMessage = new RcMessage( | ||
{ | ||
SID: rcMessage.Hdr.SID, | ||
Req: rcMessage.Hdr.Req, | ||
From: rcMessage.Hdr.To, | ||
To: rcMessage.Hdr.From, | ||
Cmd: reqid | ||
}, | ||
{ | ||
Cln: this.sipInfo.authorizationId | ||
} | ||
) | ||
const requestSipMessage = new RequestSipMessage(`MESSAGE sip:${newRcMessage.Hdr.To} SIP/2.0`, { | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${branch()}`, | ||
To: `<sip:${newRcMessage.Hdr.To}>`, | ||
From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${this.fromTag}`, | ||
'Call-ID': this.callId, | ||
'Content-Type': 'x-rc/agent' | ||
}, newRcMessage.toXml()) | ||
await this.send(requestSipMessage) | ||
} | ||
async send (sipMessage) { | ||
@@ -50,7 +78,10 @@ return new Promise((resolve, reject) => { | ||
} | ||
if (inboundSipMessage.subject === 'SIP/2.0 100 Trying') { | ||
if (inboundSipMessage.subject === 'SIP/2.0 100 Trying' || inboundSipMessage.subject === 'SIP/2.0 183 Session Progress') { | ||
return // ignore | ||
} | ||
this.off('sipMessage', responseHandler) | ||
if (inboundSipMessage.subject.startsWith('SIP/2.0 603 ')) { | ||
if (inboundSipMessage.subject.startsWith('SIP/2.0 5') || | ||
inboundSipMessage.subject.startsWith('SIP/2.0 6') || | ||
inboundSipMessage.subject.startsWith('SIP/2.0 403') | ||
) { | ||
reject(inboundSipMessage) | ||
@@ -66,2 +97,6 @@ return | ||
async response (inboundSipMessage, responseCode, headers = {}, body = '') { | ||
await this.send(new ResponseSipMessage(inboundSipMessage, responseCode, headers, body)) | ||
} | ||
async register () { | ||
@@ -97,7 +132,9 @@ const r = await this.rc.post('/restapi/v1.0/client-info/sip-provision', { | ||
const requestSipMessage = new RequestSipMessage(`REGISTER sip:${this.sipInfo.domain} SIP/2.0`, { | ||
'Call-ID': this.callerId, | ||
'Call-ID': this.callId, | ||
Contact: `<sip:${this.fakeEmail};transport=ws>;expires=600`, | ||
From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${this.fromTag}`, | ||
To: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>`, | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${this.branch()}` | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${branch()}`, | ||
Allow: 'ACK,CANCEL,INVITE,MESSAGE,BYE,OPTIONS,INFO,NOTIFY,REFER', | ||
Supported: 'path, gruu, outbound' | ||
}) | ||
@@ -108,4 +145,8 @@ let inboundSipMessage = await this.send(requestSipMessage) | ||
const nonce = wwwAuth.match(/, nonce="(.+?)"/)[1] | ||
requestSipMessage.headers.Authorization = generateAuthorization(this.sipInfo, 'REGISTER', nonce) | ||
inboundSipMessage = await this.send(requestSipMessage) | ||
const newRequestSipMessage = requestSipMessage.fork() | ||
newRequestSipMessage.headers.Authorization = generateAuthorization(this.sipInfo, 'REGISTER', nonce) | ||
inboundSipMessage = await this.send(newRequestSipMessage) | ||
if (inboundSipMessage.subject === 'SIP/2.0 200 OK') { | ||
this.emit('registered') | ||
} | ||
} | ||
@@ -116,4 +157,4 @@ } | ||
async answer (inputAudioStream = undefined) { | ||
const sdp = this.inviteSipMessage.body | ||
async answer (inviteSipMessage, inputAudioStream = undefined) { | ||
const sdp = inviteSipMessage.body | ||
const remoteRtcSd = new RTCSessionDescription({ type: 'offer', sdp }) | ||
@@ -131,13 +172,80 @@ const peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:74.125.194.127:19302' }] }) | ||
peerConnection.setLocalDescription(localRtcSd) | ||
await this.send(new ResponseSipMessage(this.inviteSipMessage, 200, 'OK', { | ||
await this.response(inviteSipMessage, 200, { | ||
Contact: `<sip:${this.fakeEmail};transport=ws>`, | ||
'Content-Type': 'application/sdp' | ||
}, localRtcSd.sdp)) | ||
}, localRtcSd.sdp) | ||
} | ||
async reject () { | ||
async toVoicemail (inviteSipMessage) { | ||
await this.sendRcMessage(inviteSipMessage, 11) | ||
} | ||
async ignore (inviteSipMessage) { | ||
await this.response(inviteSipMessage, 480) | ||
} | ||
async invite (callee, inputAudioStream = undefined) { | ||
this.newCallId() | ||
const peerConnection = new RTCPeerConnection({ | ||
iceServers: [{ urls: 'stun:74.125.194.127:19302' }] | ||
}) | ||
/* this is for debugging - start */ | ||
// const eventNames = [ | ||
// 'addstream', 'connectionstatechange', 'datachannel', 'icecandidate', | ||
// 'iceconnectionstatechange', 'icegatheringstatechange', 'identityresult', | ||
// 'negotiationneeded', 'removestream', 'signalingstatechange', 'track' | ||
// ] | ||
// for (const eventName of eventNames) { | ||
// peerConnection.addEventListener(eventName, e => { | ||
// console.log(`\n****** RTCPeerConnection ${eventName} event - start *****`) | ||
// console.log(e) | ||
// console.log(`****** RTCPeerConnection ${eventName} event - end *****\n`) | ||
// }) | ||
// } | ||
/* this is for debugging - end */ | ||
if (inputAudioStream) { | ||
const track = inputAudioStream.getAudioTracks()[0] | ||
peerConnection.addTrack(track, inputAudioStream) | ||
} | ||
const localRtcSd = await peerConnection.createOffer() | ||
peerConnection.setLocalDescription(localRtcSd) | ||
const requestSipMessage = new RequestSipMessage(`INVITE sip:${callee}@${this.sipInfo.domain} SIP/2.0`, { | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${branch()}`, | ||
To: `<sip:${callee}@${this.sipInfo.domain}>`, | ||
From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${this.fromTag}`, | ||
'Call-ID': this.callId, | ||
Contact: `<sip:${this.fakeEmail};transport=ws;ob>`, | ||
'Content-Type': 'application/sdp', | ||
// 'P-rc-country-id': 1, | ||
// 'P-rc-endpoint-id': uuid(), | ||
// 'Client-id': process.env.RINGCENTRAL_CLIENT_ID, | ||
// 'P-Asserted-Identity': 'sip:+16504223279@sip.ringcentral.com', | ||
Allow: 'ACK,CANCEL,INVITE,MESSAGE,BYE,OPTIONS,INFO,NOTIFY,REFER', | ||
Supported: 'outbound' | ||
}, localRtcSd.sdp) | ||
let inboundSipMessage = await this.send(requestSipMessage) | ||
const wwwAuth = inboundSipMessage.headers['Proxy-Authenticate'] | ||
if (wwwAuth && wwwAuth.includes(', nonce="')) { // authorization required | ||
const ackMessage = new RequestSipMessage(`ACK ${inboundSipMessage.headers.Contact.match(/<(.+?)>/)[1]} SIP/2.0`, { | ||
Via: `SIP/2.0/WSS ${this.fakeDomain};branch=${branch()}`, | ||
To: inboundSipMessage.headers.To, | ||
From: inboundSipMessage.headers.From, | ||
'Call-ID': this.callId, | ||
Supported: 'outbound' | ||
}) | ||
ackMessage.reuseCseq() | ||
this.send(ackMessage) | ||
const nonce = wwwAuth.match(/, nonce="(.+?)"/)[1] | ||
const newRequestSipMessage = requestSipMessage.fork() | ||
newRequestSipMessage.headers['Proxy-Authorization'] = generateProxyAuthorization(this.sipInfo, 'INVITE', callee, nonce) | ||
inboundSipMessage = await this.send(newRequestSipMessage) | ||
const remoteRtcSd = new RTCSessionDescription({ type: 'answer', sdp: inboundSipMessage.body }) | ||
peerConnection.addEventListener('track', e => { | ||
this.emit('track', e) | ||
}) | ||
peerConnection.setRemoteDescription(remoteRtcSd) | ||
} | ||
} | ||
} | ||
export default Softphone |
import md5 from 'blueimp-md5' | ||
import uuid from 'uuid/v4' | ||
@@ -30,1 +31,3 @@ const generateResponse = (username, password, realm, method, uri, nonce) => { | ||
} | ||
export const branch = () => 'z9hG4bK' + uuid() |
@@ -7,7 +7,7 @@ import HtmlWebpackPlugin from 'html-webpack-plugin' | ||
entry: { | ||
index: './demos/browser.js' | ||
index: './demos/browser/index.js' | ||
}, | ||
plugins: [ | ||
new HtmlWebpackPlugin({ | ||
template: 'demos/index.html' | ||
template: 'demos/browser/index.html' | ||
}), | ||
@@ -14,0 +14,0 @@ new HotModuleReplacementPlugin(), |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
110612
51
1592
106
32
1