diff --git a/.helm/Chart.yaml b/.helm/Chart.yaml index c5e9217d6..e345e231f 100644 --- a/.helm/Chart.yaml +++ b/.helm/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: routr description: Next-generation SIP Server type: application -version: 0.0.6 +version: 0.0.7 appVersion: 1.0.0-rc5 dependencies: - name: redis diff --git a/.helm/README.md b/.helm/README.md index b279e25c6..a56ed9cea 100644 --- a/.helm/README.md +++ b/.helm/README.md @@ -92,6 +92,7 @@ The following table lists the configurable parameters of the Routr chart and the | routr.localnets | Local networks in CIDR format. Use in combination with `externAddr` | [] | | routr.recordRoute | Stay within the signaling path | `false` | | routr.useToAsAOR | Uses the `To` header, instead of `Request-URI`, to locate endpoints | `false` | +| routr.patchRequestURI | Uses the user part of the `To` header to ammend the `Request-URI` if it doesn't have user| `false` | | routr.registrarIntf | `Internal` causes the server to use the IP and port it "sees"(received & rport) from a device attempting to register | `External` | | routr.accessControlList.deny | Deny incoming traffic from network list. Must be valid CIDR values | [] | | routr.accessControlList.allow | Allow incoming traffic from network list. Must be valid CIDR values | [] | diff --git a/.helm/templates/deployment.yaml b/.helm/templates/deployment.yaml index 2266cf71a..c3013e1fc 100644 --- a/.helm/templates/deployment.yaml +++ b/.helm/templates/deployment.yaml @@ -48,6 +48,8 @@ spec: value: {{ .Values.routr.recordRoute | quote }} - name: USE_TO_AS_AOR value: {{ .Values.routr.useToAsAOR | quote }} + - name: PATCH_REQUEST_URI + value: {{ .Values.routr.patchRequestURI | quote }} - name: REGISTRAR_INTF value: {{ .Values.routr.registrarIntf | quote }} - name: ACCESS_CONTROL_LIST_ALLOW diff --git a/.helm/templates/service.yml b/.helm/templates/service.yml index 6b3fe7ef6..e756d31ba 100644 --- a/.helm/templates/service.yml +++ b/.helm/templates/service.yml @@ -6,6 +6,10 @@ metadata: labels: {{- include ".helm2.labels" . | nindent 4 }} namespace: {{ .Release.Namespace }} +{{- with .Values.adminService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} spec: type: {{ .Values.adminService.type }} ports: @@ -25,6 +29,10 @@ metadata: labels: {{- include ".helm2.labels" . | nindent 4 }} namespace: {{ .Release.Namespace }} +{{- with .Values.udpSignalingService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} spec: type: {{ .Values.udpSignalingService.type }} {{- if ne .Values.udpSignalingService.type "ClusterIP" }} @@ -48,6 +56,10 @@ metadata: labels: {{- include ".helm2.labels" . | nindent 4 }} namespace: {{ .Release.Namespace }} +{{- with .Values.tcpSignalingService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} spec: type: {{ .Values.tcpSignalingService.type }} {{- if ne .Values.tcpSignalingService.type "ClusterIP" }} diff --git a/.helm/values.yaml b/.helm/values.yaml index dc12d0c3b..6a77cfd2c 100644 --- a/.helm/values.yaml +++ b/.helm/values.yaml @@ -15,6 +15,7 @@ adminService: name: api type: ClusterIP port: 4567 + annotations: {} # Use this service to enable UDP access to the server's signaling # capabilities. Keep in mind that this will create the service for you @@ -25,6 +26,7 @@ udpSignalingService: name: sipudp type: ClusterIP port: 5060 + annotations: {} # Use this service to enable TCP access to the server's signaling # capabilities. Keep in mind that this will create the service for you @@ -37,6 +39,7 @@ tcpSignalingService: ports: - name: siptcp port: 5060 + annotations: {} # Routr internal configurations routr: @@ -46,6 +49,7 @@ routr: localnets: [] recordRoute: false useToAsAOR: false + patchRequestURI: false registrarIntf: External accessControlList: deny: [] @@ -80,8 +84,6 @@ redis: mountPath: /bitnami/redis size: 5Gi -# Omiting this for now - replicaCount: 1 nameOverride: "" fullnameOverride: "" diff --git a/build.gradle b/build.gradle index 097e58b71..b42722bbe 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,7 @@ dependencies { compile 'io.kotest:kotest-runner-junit5-jvm:4.0.3' compile 'io.kotest:kotest-assertions-core-jvm:4.0.3' compile 'io.kotest:kotest-property-jvm:4.0.3' + compile "com.mashape.unirest:unirest-java:1.4.9" implementation 'org.jetbrains.kotlin:kotlin-reflect:1.3.50' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' } diff --git a/config/config.yml b/config/config.yml index 606abb512..2ea66ee28 100644 --- a/config/config.yml +++ b/config/config.yml @@ -5,3 +5,9 @@ spec: port: 5060 - protocol: udp port: 5060 + - protocol: tls + port: 5061 + - protocol: ws + port: 5062 + - protocol: wss + port: 5063 \ No newline at end of file diff --git a/etc/schemas/config_schema.json b/etc/schemas/config_schema.json index 60f143fc4..092298084 100644 --- a/etc/schemas/config_schema.json +++ b/etc/schemas/config_schema.json @@ -58,6 +58,15 @@ "trustStorePassword": { "type": "string" } } }, + "ex_rtpEngine": { + "type": "object", + "properties": { + "port": { "type": "integer" }, + "host": { "type": "string" }, + "proto": { "type": "string" } + }, + "required": ["host"] + }, "dataSource": { "type": "object", "properties": { diff --git a/mod/core/config/config_defaults.js b/mod/core/config/config_defaults.js index d1eae7d70..67c89bb90 100644 --- a/mod/core/config/config_defaults.js +++ b/mod/core/config/config_defaults.js @@ -23,6 +23,38 @@ module.exports = upSince => { dataSource: { provider: 'files_data_provider' }, + ex_rtpEngine: { + enabled: false, + proto: 'http', + port: 8080, + bridgeParams: { + webToWeb: { + ICE: 'force', + SDES: 'off', + flags: 'trust-address replace-origin replace-session-connection' + }, + webToSip: { + 'transport-protocol': 'RTP/AVP', + 'rtcp-mux': 'demux', + ICE: 'remove', + flags: 'trust-address replace-origin replace-session-connection' + }, + sipToWeb: { + 'transport-protocol': 'UDP/TLS/RTP/SAVP', + 'rtcp-mux': 'offer', + ICE: 'force', + SDES: 'off', + flags: + 'trust-address replace-origin replace-session-connection generate-mid' + }, + sipToSip: { + 'transport-protocol': 'RTP/AVP', + 'rtcp-mux': 'demux', + ICE: 'remove', + flags: 'trust-address replace-origin replace-session-connection' + } + } + }, registrarIntf: 'External', restService: { keyStore: 'etc/certs/api-cert.jks', diff --git a/mod/core/config/config_from_env.js b/mod/core/config/config_from_env.js index 95836aa63..6ed7059b2 100644 --- a/mod/core/config/config_from_env.js +++ b/mod/core/config/config_from_env.js @@ -45,6 +45,10 @@ envsMap.set( 'spec.securityContext.client.authType' ) envsMap.set('SECURITY_CONTEXT_DEBUGGING', 'spec.securityContext.debugging') +envsMap.set('EX_RTP_ENGINE_ENABLED', 'spec.ex_rtpEngine.enabled') +envsMap.set('EX_RTP_ENGINE_PROTO', 'spec.ex_rtpEngine.proto') +envsMap.set('EX_RTP_ENGINE_HOST', 'spec.ex_rtpEngine.host') +envsMap.set('EX_RTP_ENGINE_PORT', 'spec.ex_rtpEngine.port') envsMap.set('LOG4J', '') envsMap.set('CONFIG_FILE', '') envsMap.set('SALT', '') diff --git a/mod/core/processor/processor.js b/mod/core/processor/processor.js index aae397feb..82291b7b1 100644 --- a/mod/core/processor/processor.js +++ b/mod/core/processor/processor.js @@ -5,6 +5,8 @@ const postal = require('postal') const RequestProcessor = require('@routr/core/processor/request_processor') const ResponseProcessor = require('@routr/core/processor/response_processor') +const CallIdHeader = Java.type('javax.sip.header.CallIdHeader') +const FromHeader = Java.type('javax.sip.header.FromHeader') const SipListener = Java.type('javax.sip.SipListener') const LogManager = Java.type('org.apache.logging.log4j.LogManager') const LOG = LogManager.getLogger() @@ -45,22 +47,29 @@ class Processor { channel: 'processor', topic: 'transaction.timeout', data: { - transactionId: transactionId, + transactionId, isServerTransaction: event.isServerTransaction() } }) }, processTransactionTerminated: event => { + const request = event.getServerTransaction().getRequest() + const callId = request.getHeader(CallIdHeader.NAME).getCallId() + const fromTag = request.getHeader(FromHeader.NAME).getTag() + const transactionId = event.isServerTransaction() ? event.getServerTransaction().getBranchId() : event.getClientTransaction().getBranchId() + postal.publish({ channel: 'processor', topic: 'transaction.terminated', data: { - transactionId: transactionId, - isServerTransaction: event.isServerTransaction() + transactionId, + isServerTransaction: event.isServerTransaction(), + callId, + fromTag } }) }, diff --git a/mod/core/processor/processor_utils.js b/mod/core/processor/processor_utils.js index b05462124..18542210d 100644 --- a/mod/core/processor/processor_utils.js +++ b/mod/core/processor/processor_utils.js @@ -6,10 +6,13 @@ const AuthHelper = require('@routr/utils/auth_helper') const { connectionException } = require('@routr/utils/exception_helpers') const ToHeader = Java.type('javax.sip.header.ToHeader') +const FromHeader = Java.type('javax.sip.header.FromHeader') const ContactHeader = Java.type('javax.sip.header.ContactHeader') const ExpiresHeader = Java.type('javax.sip.header.ExpiresHeader') const CSeqHeader = Java.type('javax.sip.header.CSeqHeader') const ViaHeader = Java.type('javax.sip.header.ViaHeader') +const ContentType = Java.type('javax.sip.header.ContentTypeHeader') +const CallIdHeader = Java.type('javax.sip.header.CallIdHeader') const Request = Java.type('javax.sip.message.Request') const Response = Java.type('javax.sip.message.Response') const AccountManager = Java.type( @@ -22,6 +25,15 @@ const SipFactory = Java.type('javax.sip.SipFactory') const headerFactory = SipFactory.getInstance().createHeaderFactory() const messageFactory = SipFactory.getInstance().createMessageFactory() +const extractRTPEngineParams = r => { + return { + sdp: String.fromCharCode.apply(null, r.getContent()), + 'call-id': r.getHeader(CallIdHeader.NAME).getCallId(), + 'from-tag': r.getHeader(FromHeader.NAME).getTag(), + 'to-tag': r.getHeader(ToHeader.NAME).getTag() + } +} +const hasSDP = r => r.getHeader(ContentType.NAME) != null const hasCodes = (r, c) => c.filter(code => r.getStatusCode() === code).length > 0 const isMethod = (r, m) => @@ -142,3 +154,5 @@ module.exports.isRegisterNok = isRegisterNok module.exports.isBehindNat = isBehindNat module.exports.handleAuthChallenge = handleAuthChallenge module.exports.getExpires = getExpires +module.exports.hasSDP = hasSDP +module.exports.extractRTPEngineParams = extractRTPEngineParams diff --git a/mod/core/processor/request_handler.js b/mod/core/processor/request_handler.js index ddf8f887b..41cbde25f 100644 --- a/mod/core/processor/request_handler.js +++ b/mod/core/processor/request_handler.js @@ -3,9 +3,18 @@ * @since v1 */ const { connectionException } = require('@routr/utils/exception_helpers') -const { sendResponse } = require('@routr/core/processor/processor_utils') +const { + sendResponse, + hasSDP, + extractRTPEngineParams +} = require('@routr/core/processor/processor_utils') const { Status } = require('@routr/core/status') const config = require('@routr/core/config_util')() +const RTPEngineConnector = require('@routr/rtpengine/connector') +const ContentTypeHeader = Java.type('javax.sip.header.ContentTypeHeader') +const CallIdHeader = Java.type('javax.sip.header.CallIdHeader') +const FromHeader = Java.type('javax.sip.header.FromHeader') +const Request = Java.type('javax.sip.message.Request') const postal = require('postal') const { @@ -15,7 +24,6 @@ const { configureProxyAuthorization, configureRequestURI, configureMaxForwards, - configureContact, configurePrivacy, configureRecordRoute, configureIdentity, @@ -23,16 +31,18 @@ const { configureCSeq, isInDialog } = require('@routr/core/processor/request_utils') +const { directionFromRequest } = require('@routr/rtpengine/utils') const { RoutingType } = require('@routr/core/routing_type') const ObjectId = Java.type('org.bson.types.ObjectId') const Request = Java.type('javax.sip.message.Request') const Response = Java.type('javax.sip.message.Response') -const RouteHeader = Java.type('javax.sip.header.RouteHeader') const ViaHeader = Java.type('javax.sip.header.ViaHeader') const ToHeader = Java.type('javax.sip.header.ToHeader') const LogManager = Java.type('org.apache.logging.log4j.LogManager') const ConcurrentHashMap = Java.type('java.util.concurrent.ConcurrentHashMap') const requestStore = new ConcurrentHashMap() +const isInviteOrAck = r => + r.getMethod() === Request.INVITE || r.getMethod() === Request.ACK const LOG = LogManager.getLogger() @@ -40,11 +50,13 @@ class RequestHandler { constructor (sipProvider, contextStorage) { this.sipProvider = sipProvider this.contextStorage = contextStorage + if (config.spec.ex_rtpEngine.enabled) + this.rtpeConnector = new RTPEngineConnector(config.spec.ex_rtpEngine) postal.subscribe({ channel: 'locator', topic: 'endpoint.find.reply', - callback: (data, envelope) => { + callback: data => { const requestInfo = requestStore.get(data.requestId) if (requestInfo === null) return @@ -96,50 +108,101 @@ class RequestHandler { } } - processRoute (transaction, request, route, routeInfo) { - const transport = request - .getHeader(ViaHeader.NAME) - .getTransport() - .toLowerCase() - const lp = this.sipProvider.getListeningPoint(transport) - const localAddr = { host: lp.getIPAddress().toString(), port: lp.getPort() } - - const advertisedAddr = getAdvertisedAddr(request, route, localAddr) - - let requestOut = configureMaxForwards(request) - requestOut = configureProxyAuthorization(requestOut) - requestOut = configureRoute(requestOut, localAddr) - requestOut = configureVia(requestOut, advertisedAddr) - //requestOut = configureContact(requestOut) - - if (!isInDialog(request)) { - requestOut = configureRequestURI(requestOut, routeInfo, route) - requestOut = configurePrivacy(requestOut, routeInfo) - requestOut = configureIdentity(requestOut, route) - requestOut = configureXHeaders(requestOut, route) - requestOut = configureRecordRoute(requestOut, advertisedAddr, localAddr) - } + async processRoute (transaction, request, route, routeInfo) { + try { + const lpTransport = request + .getHeader(ViaHeader.NAME) + .getTransport() + .toLowerCase() + const targetTransport = route + ? route.transport + : request + .getRequestURI() + .getParameter('transport') + .toLowerCase() + + const lp = this.sipProvider.getListeningPoint(lpTransport) + const localAddr = { + host: lp.getIPAddress().toString(), + port: lp.getPort(), + transport: lpTransport + } + const advertisedAddr = getAdvertisedAddr( + request, + route, + localAddr, + targetTransport + ) - if (routeInfo.getRoutingType() === RoutingType.DOMAIN_EGRESS_ROUTING) { - // XXX: Please document this situation :( - requestOut = configureCSeq(requestOut) - } + LOG.debug( + `core.processor.RequestHandler.processRoute [targetTransport = ${targetTransport}]` + ) + LOG.debug( + `core.processor.RequestHandler.processRoute [lpTransport = ${lpTransport}]` + ) - LOG.debug( - `core.processor.RequestHandler.processRoute [advertised addr ${JSON.stringify( - advertisedAddr - )}]` - ) - LOG.debug( - `core.processor.RequestHandler.processRoute [route ${JSON.stringify( - route - )}]` - ) - - this.sendRequest(transaction, request, requestOut) + let requestOut = configureMaxForwards(request) + requestOut = configureProxyAuthorization(requestOut) + requestOut = configureRoute(requestOut, localAddr) + requestOut = configureVia(requestOut, advertisedAddr, targetTransport) + //requestOut = configureContact(requestOut) + + if (!isInDialog(request)) { + requestOut = configureRequestURI(requestOut, routeInfo, route) + requestOut = configurePrivacy(requestOut, routeInfo) + requestOut = configureIdentity(requestOut, route) + requestOut = configureXHeaders(requestOut, route) + requestOut = configureRecordRoute(requestOut, localAddr, advertisedAddr) + } + + if (routeInfo.getRoutingType() === RoutingType.DOMAIN_EGRESS_ROUTING) { + // XXX: Please document this situation :( + requestOut = configureCSeq(requestOut) + } + + LOG.debug( + `core.processor.RequestHandler.processRoute [advertised addr ${JSON.stringify( + advertisedAddr + )}]` + ) + LOG.debug( + `core.processor.RequestHandler.processRoute [route ${JSON.stringify( + route + )}]` + ) + + let bridgingNote + if ( + config.spec.ex_rtpEngine.enabled && + isInviteOrAck(request) && + hasSDP(request) + ) { + // The note must be taken from the original request else it won't + // have the correct transport. + bridgingNote = directionFromRequest(request, route) + const obj = await this.rtpeConnector.offer( + bridgingNote, + extractRTPEngineParams(request) + ) + requestOut.setContent( + obj.sdp, + requestOut.getHeader(ContentTypeHeader.NAME) + ) + } + + if (request.getMethod() === Request.BYE) { + const callId = request.getHeader(CallIdHeader.NAME).getCallId() + const fromTag = request.getHeader(FromHeader.NAME).getTag() + await this.rtpeConnector.delete(callId, fromTag) + } + + this.sendRequest(transaction, request, requestOut, bridgingNote) + } catch (e) { + LOG.error(e) + } } - sendRequest (serverTransaction, request, requestOut) { + sendRequest (serverTransaction, request, requestOut, bridgingNote) { // Does not need a transaction if (request.getMethod().equals(Request.ACK)) { return this.sipProvider.sendRequest(requestOut) @@ -162,14 +225,21 @@ class RequestHandler { request, requestOut, clientTransaction, - serverTransaction + serverTransaction, + bridgingNote ) } catch (e) { connectionException(e, requestOut.getRequestURI().getHost()) } } - saveContext (request, requestOut, clientTransaction, serverTransaction) { + saveContext ( + request, + requestOut, + clientTransaction, + serverTransaction, + bridgingNote + ) { // Transaction context const context = {} context.clientTransaction = clientTransaction @@ -177,6 +247,7 @@ class RequestHandler { context.method = request.getMethod() context.requestIn = request context.requestOut = requestOut + context.bridgingNote = bridgingNote this.contextStorage.addContext(context) } } diff --git a/mod/core/processor/request_processor.js b/mod/core/processor/request_processor.js index 7b75dc3de..ec093e7c2 100644 --- a/mod/core/processor/request_processor.js +++ b/mod/core/processor/request_processor.js @@ -13,7 +13,6 @@ const { RoutingType } = require('@routr/core/routing_type') const { RouteEntityType } = require('@routr/core/route_entity_type') const { Status } = require('@routr/core/status') const config = require('@routr/core/config_util')() - const Request = Java.type('javax.sip.message.Request') const Response = Java.type('javax.sip.message.Response') const LogManager = Java.type('org.apache.logging.log4j.LogManager') @@ -29,7 +28,7 @@ class RequestProcessor { this.domainsAPI = dataAPIs.DomainsAPI } - process (event) { + async process (event) { const request = event.getRequest() let transaction = event.getServerTransaction() diff --git a/mod/core/processor/request_utils.js b/mod/core/processor/request_utils.js index 6b29200c1..d57166328 100644 --- a/mod/core/processor/request_utils.js +++ b/mod/core/processor/request_utils.js @@ -34,7 +34,7 @@ const ownedAddresss = localAddr => } ] : [localAddr] -const getAdvertisedAddr = (request, route, localAddr) => { +const getAdvertisedAddr = (request, route, localAddr, targetTransport) => { // After the initial invite the route object will be null // and we need to the the target address from the request uri. // If the routing is type IDR the initial request uri will be a local @@ -44,9 +44,13 @@ const getAdvertisedAddr = (request, route, localAddr) => { ? LocatorUtils.aorAsObj(route.contactURI).getHost() : request.getRequestURI().getHost() const externAddr = config.spec.externAddr - return config.spec.externAddr && needsExternAddress(route, targetAddr) - ? { host: addrHost(externAddr), port: addrPort(externAddr, localAddr) } - : localAddr + return externAddr && needsExternAddress(route, targetAddr) + ? { + host: addrHost(externAddr), + port: addrPort(externAddr, localAddr), + transport: targetTransport + } + : { host: localAddr.host, port: localAddr.port, transport: targetTransport } } const getToUser = request => { const toHeader = request.getHeader(ToHeader.NAME) @@ -116,12 +120,7 @@ const configureRoute = (request, localAddr) => { } return requestOut } -const configureVia = (request, advertisedAddr) => { - const requestOut = request.clone() - const transport = requestOut - .getHeader(ViaHeader.NAME) - .getTransport() - .toLowerCase() +const configureVia = (request, advertisedAddr, transport) => { const viaHeader = headerFactory.createViaHeader( advertisedAddr.host, advertisedAddr.port, @@ -129,30 +128,35 @@ const configureVia = (request, advertisedAddr) => { null ) viaHeader.setRPort() + const requestOut = request.clone() requestOut.addFirst(viaHeader) return requestOut } -const configureRecordRoute = (request, advertisedAddr, localAddr) => { + +// rfc5658 +const configureRecordRoute = (request, localAddr, advertisedAddr) => { const requestOut = request.clone() - if (config.spec.recordRoute) { + const viaHeader = request.getHeaders(ViaHeader.NAME).next() + const transport = viaHeader.getTransport().toLowerCase() + + if (config.spec.recordRoute || transport === 'ws' || transport === 'wss') { + // First we need the input interface from the top ViaHeader const p1 = addressFactory.createSipURI(null, localAddr.host) - p1.setLrParam() p1.setPort(localAddr.port) + p1.setTransportParam(localAddr.transport) + p1.setLrParam() const pa1 = addressFactory.createAddress(p1) const rr1 = headerFactory.createRecordRouteHeader(pa1) requestOut.addHeader(rr1) - if (config.spec.externAddr && isPublicAddress(advertisedAddr.host)) { - const p2 = addressFactory.createSipURI( - null, - addrHost(config.spec.externAddr) - ) - p2.setLrParam() - p2.setPort(addrPort(config.spec.externAddr, localAddr)) - const pa2 = addressFactory.createAddress(p2) - const rr2 = headerFactory.createRecordRouteHeader(pa2) - requestOut.addFirst(rr2) - } + // Then we get the advertisedAddr + const p2 = addressFactory.createSipURI(null, advertisedAddr.host) + p2.setLrParam() + p2.setTransportParam(advertisedAddr.transport) + p2.setPort(advertisedAddr.port) + const pa2 = addressFactory.createAddress(p2) + const rr2 = headerFactory.createRecordRouteHeader(pa2) + requestOut.addLast(rr2) } return requestOut } diff --git a/mod/core/processor/response_processor.js b/mod/core/processor/response_processor.js index c4b0f6142..80bb559ee 100644 --- a/mod/core/processor/response_processor.js +++ b/mod/core/processor/response_processor.js @@ -8,19 +8,29 @@ const { isStackJob, isTransactional, mustAuthenticate, - handleAuthChallenge + handleAuthChallenge, + hasSDP, + extractRTPEngineParams, + isOk } = require('@routr/core/processor/processor_utils') +const RTPEngineConnector = require('@routr/rtpengine/connector') const ViaHeader = Java.type('javax.sip.header.ViaHeader') +const ContentTypeHeader = Java.type('javax.sip.header.ContentTypeHeader') const SipFactory = Java.type('javax.sip.SipFactory') const LogManager = Java.type('org.apache.logging.log4j.LogManager') const LOG = LogManager.getLogger() const headerFactory = SipFactory.getInstance().createHeaderFactory() +const { RTPBridgingNote } = require('@routr/rtpengine/rtp_bridging_note') +const { directionFromResponse } = require('@routr/rtpengine/utils') +const config = require('@routr/core/config_util')() class ResponseProcessor { constructor (sipProvider, contextStorage) { this.sipProvider = sipProvider this.contextStorage = contextStorage this.gatewaysAPI = new GatewaysAPI(DSSelector.getDS()) + if (config.spec.ex_rtpEngine.enabled) + this.rtpeConnector = new RTPEngineConnector(config.spec.ex_rtpEngine) } process (event) { @@ -41,35 +51,73 @@ class ResponseProcessor { this.sendResponse(event) } - sendResponse (event) { - const response = event.getResponse().clone() - const viaHeader = response.getHeader(ViaHeader.NAME) - const xReceivedHeader = headerFactory.createHeader( - 'X-Inf-Received', - viaHeader.getReceived() - ) - const xRPortHeader = headerFactory.createHeader( - 'X-Inf-RPort', - `${viaHeader.getRPort()}` - ) - response.addHeader(xReceivedHeader) - response.addHeader(xRPortHeader) - response.removeFirst(ViaHeader.NAME) - if (isTransactional(event)) { - const context = this.contextStorage.findContext( - event.getClientTransaction().getBranchId() + async sendResponse (event) { + try { + const response = event.getResponse().clone() + const bridgingNote = directionFromResponse(response) + const viaHeader = response.getHeader(ViaHeader.NAME) + const xReceivedHeader = headerFactory.createHeader( + 'X-Inf-Received', + viaHeader.getReceived() ) + const xRPortHeader = headerFactory.createHeader( + 'X-Inf-RPort', + `${viaHeader.getRPort()}` + ) + response.addHeader(xReceivedHeader) + response.addHeader(xRPortHeader) + response.removeFirst(ViaHeader.NAME) + + if (isTransactional(event)) { + const context = this.contextStorage.findContext( + event.getClientTransaction().getBranchId() + ) + + // WARNINIG: We need to remove the SDP for response to WebRTC endpoints + // else we will get a Called with SDP without DTLS fingerprint + if ( + config.spec.ex_rtpEngine.enabled && + response.getStatusCode() === 183 && + bridgingNote === RTPBridgingNote.WEB_TO_SIP + ) + response.removeContent() + + if ( + config.spec.ex_rtpEngine.enabled && + isOk(response) && + hasSDP(response) + ) { + const obj = await this.rtpeConnector.answer( + bridgingNote, + extractRTPEngineParams(response) + ) + + // WARNINIG: This patches an issue with RTPEngine where its not setting rtpmux + if (bridgingNote === RTPBridgingNote.WEB_TO_SIP) + obj.sdp = obj.sdp.replace( + 'a=setup:active', + 'a=setup:active\na=rtcp-mux' + ) + + response.setContent( + obj.sdp, + response.getHeader(ContentTypeHeader.NAME) + ) + } - if (context && context.serverTransaction) { - context.serverTransaction.sendResponse(response) + if (context && context.serverTransaction) { + context.serverTransaction.sendResponse(response) + } else if (response.getHeader(ViaHeader.NAME) !== null) { + this.sipProvider.sendResponse(response) + } } else if (response.getHeader(ViaHeader.NAME) !== null) { + // Could be a BYE due to Record-Route this.sipProvider.sendResponse(response) } - } else if (response.getHeader(ViaHeader.NAME) !== null) { - // Could be a BYE due to Record-Route - this.sipProvider.sendResponse(response) + LOG.debug(response) + } catch (e) { + LOG.error(e) } - LOG.debug(response) } } diff --git a/mod/core/processor/route_info.js b/mod/core/processor/route_info.js index 3b7baf75b..5b099f198 100644 --- a/mod/core/processor/route_info.js +++ b/mod/core/processor/route_info.js @@ -17,7 +17,6 @@ class RouteInfo { constructor (request, dataAPIs) { const fromHeader = request.getHeader(FromHeader.NAME) const toHeader = request.getHeader(ToHeader.NAME) - const sipFactory = SipFactory.getInstance() this.request = request this._callerUser = fromHeader .getAddress() diff --git a/mod/core/processor/test.js b/mod/core/processor/test.js index 869b58a21..4ff6fbb7b 100644 --- a/mod/core/processor/test.js +++ b/mod/core/processor/test.js @@ -81,8 +81,6 @@ describe('Core Processor Module', () => { }) it('Response utils', function (done) { - const CSeqHeader = Java.type('javax.sip.header.CSeqHeader') - const ViaHeader = Java.type('javax.sip.header.ViaHeader') const Request = Java.type('javax.sip.message.Request') const Response = Java.type('javax.sip.message.Response') diff --git a/mod/core/server.js b/mod/core/server.js index 9719a56d4..8d9383edc 100644 --- a/mod/core/server.js +++ b/mod/core/server.js @@ -24,7 +24,6 @@ const System = Java.type('java.lang.System') const SipFactory = Java.type('javax.sip.SipFactory') const LogManager = Java.type('org.apache.logging.log4j.LogManager') const LogOutputStream = Java.type('io.routr.core.LogOutputStream') -const OutputStream = Java.type('java.io.OutputStream') const PrintStream = Java.type('java.io.PrintStream') const LOG = LogManager.getLogger() @@ -50,6 +49,7 @@ class Server { this.dataAPIs = dataAPIs this.locator = new Locator() + //new RTPEngineConnector() } buildSipProvider (sipStack, transport) { diff --git a/mod/data_api/domains.unit.test.js b/mod/data_api/domains.unit.test.js index 38998df3f..ba4e3d509 100644 --- a/mod/data_api/domains.unit.test.js +++ b/mod/data_api/domains.unit.test.js @@ -4,7 +4,6 @@ * * Unit Test for the "Domains API" */ -const APIBase = require('@routr/data_api/api_base') const DomainsAPI = require('@routr/data_api/domains_api') const TestUtils = require('@routr/data_api/test_utils') const chai = require('chai') diff --git a/mod/location/location.unit.test.js b/mod/location/location.unit.test.js index 5bc8196be..1cda075ce 100644 --- a/mod/location/location.unit.test.js +++ b/mod/location/location.unit.test.js @@ -5,7 +5,6 @@ * Unit Test for the configuration utility */ const chai = require('chai') -const sinon = require('sinon') const sinonChai = require('sinon-chai') chai.use(sinonChai) const expect = chai.expect diff --git a/mod/location/locator.js b/mod/location/locator.js index b6c65490c..3fc8a5567 100644 --- a/mod/location/locator.js +++ b/mod/location/locator.js @@ -32,7 +32,6 @@ class Locator { addEndpoint (addressOfRecord, route) { // This must be done here before we convert contactURI into a string - const contactURI = LocatorUtils.aorAsString(route.contactURI) route.contactURI = route.contactURI.toString() LOG.debug( @@ -77,8 +76,8 @@ class Locator { if (addressOfRecord.startsWith('tel:')) { return this.findEndpointByTelUrl(addressOfRecord) } else { - const tel = LocatorUtils.aorAsObj(addressOfRecord).getUser() try { + const tel = LocatorUtils.aorAsObj(addressOfRecord).getUser() const telE164 = phone(tel)[0] const response = this.findEndpointByTelUrl(`tel:${telE164}`) if (response.status === Status.OK) return response diff --git a/mod/location/route_loader.js b/mod/location/route_loader.js index e15407d4e..71d1af810 100644 --- a/mod/location/route_loader.js +++ b/mod/location/route_loader.js @@ -22,8 +22,6 @@ class RouteLoader { } getDomainEgressRoutes (domainsAPI, numbersAPI, gatewaysAPI) { - const SipFactory = Java.type('javax.sip.SipFactory') - const addressFactory = SipFactory.getInstance().createAddressFactory() const routes = new HashMap() const domains = domainsAPI.getDomains().data diff --git a/mod/location/utils.js b/mod/location/utils.js index 322d576f0..00b24052f 100644 --- a/mod/location/utils.js +++ b/mod/location/utils.js @@ -93,7 +93,8 @@ class LocatorUtils { gwHost: buildAddr(gateway.spec.host, gateway.spec.port), numberRef: number.metadata.ref, number: number.spec.location.telUrl.split(':')[1], - expires: -1 + expires: -1, + transport: gateway.spec.transport } if (domain) { route.rule = domain.spec.context.egressPolicy.rule diff --git a/mod/registrar/registrar.js b/mod/registrar/registrar.js index 7cfd75c59..52ad20142 100644 --- a/mod/registrar/registrar.js +++ b/mod/registrar/registrar.js @@ -5,21 +5,14 @@ const postal = require('postal') const AuthHelper = require('@routr/utils/auth_helper') const { Status } = require('@routr/core/status') -const isEmpty = require('@routr/utils/obj_util') -const getConfig = require('@routr/core/config_util') const RegistrarUtils = require('@routr/registrar/utils') const DSSelector = require('@routr/data_api/ds_selector') const AgentsAPI = require('@routr/data_api/agents_api') const PeersAPI = require('@routr/data_api/peers_api') -const ViaHeader = Java.type('javax.sip.header.ViaHeader') -const ContactHeader = Java.type('javax.sip.header.ContactHeader') const FromHeader = Java.type('javax.sip.header.FromHeader') -const ExpiresHeader = Java.type('javax.sip.header.ExpiresHeader') const AuthorizationHeader = Java.type('javax.sip.header.AuthorizationHeader') -const SipFactory = Java.type('javax.sip.SipFactory') const LogManager = Java.type('org.apache.logging.log4j.LogManager') -const addressFactory = SipFactory.getInstance().createAddressFactory() const LOG = LogManager.getLogger() diff --git a/mod/registrar/utils.js b/mod/registrar/utils.js index b5eb3d5d8..aed18707e 100644 --- a/mod/registrar/utils.js +++ b/mod/registrar/utils.js @@ -66,7 +66,11 @@ class RegistrarUtils { contactURI: RegistrarUtils.getUpdatedContactURI(request, user), registeredOn: Date.now(), expires: getExpires(request), - nat: isBehindNat(request) + nat: isBehindNat(request), + transport: request + .getHeader(ViaHeader.NAME) + .getTransport() + .toLowerCase() } } diff --git a/mod/registry/registry.js b/mod/registry/registry.js index 09d016a4f..e8db709f6 100644 --- a/mod/registry/registry.js +++ b/mod/registry/registry.js @@ -17,12 +17,7 @@ const { protocolTransport, nearestInterface } = require('@routr/utils/misc_utils') -const { - isRegistered, - isExpired, - isStaticMode, - unregistered -} = require('@routr/registry/utils') +const { isExpired, unregistered } = require('@routr/registry/utils') const LogManager = Java.type('org.apache.logging.log4j.LogManager') const LOG = LogManager.getLogger() diff --git a/mod/registry/sip_listener.js b/mod/registry/sip_listener.js index b3205dae0..2f4831e88 100644 --- a/mod/registry/sip_listener.js +++ b/mod/registry/sip_listener.js @@ -28,9 +28,9 @@ function storeRegistry (store, gwRef, gwURI, expires) { host: gwURI.getHost(), ip: InetAddress.getByName(gwURI.getHost()).getHostAddress(), //expires: actualExpires, - expires: expires, registeredOn: Date.now(), gwRef: gwRef, + expires, gwURI: gwURI.toString() } store.withCollection('registry').put(gwURI.toString(), JSON.stringify(reg)) diff --git a/mod/rtpengine/connector.js b/mod/rtpengine/connector.js new file mode 100644 index 000000000..f2e5102d8 --- /dev/null +++ b/mod/rtpengine/connector.js @@ -0,0 +1,71 @@ +/** + * Manage RTPEngine bindings + * + * @author Pedro Sanders + * @since v1 + */ +const merge = require('deepmerge') +const { RTPBridgingNote } = require('@routr/rtpengine/rtp_bridging_note') +const LogManager = Java.type('org.apache.logging.log4j.LogManager') +const LOG = LogManager.getLogger() +const NGHttpSender = require('./ng_http_sender') +const postal = require('postal') + +class RTPEngineConnector { + constructor (config) { + LOG.debug(`rtpengine.RTPEngineConnector connector is up`) + + // This shouldn't be need because there is no dialog stablish + // The call will be removed afeter a timeout + /*postal.subscribe({ + channel: 'processor', + topic: 'transaction.cancel', + callback: async(data) => { + await this.delete(data.callId, data.fromTag) + } + })*/ + this.sender = new NGHttpSender( + `${config.proto}://${config.host}:${config.port}/ng` + ) + this.bridgeParams = config.bridgeParams + } + + getBridgingInfo (bridgingNote, offer = true) { + const bridgeParams = this.bridgeParams + switch (bridgingNote) { + case RTPBridgingNote.WEB_TO_WEB: + return bridgeParams.webToWeb + case RTPBridgingNote.WEB_TO_SIP: + return offer ? bridgeParams.webToSip : bridgeParams.sipToWeb + case RTPBridgingNote.SIP_TO_WEB: + return offer ? bridgeParams.sipToWeb : bridgeParams.webToSip + default: + return bridgeParams.sipToSip + } + } + + async delete (callId, fromTag) { + return await this.sender.sendCmd('delete', { + 'call-id': callId, + 'from-tag': fromTag + }) + } + + async offer (bridgingNote, params) { + LOG.debug( + `rtpengine.RTPEngineConnector.offer [bridging note: ${bridgingNote}]` + ) + const p = merge(params, this.getBridgingInfo(bridgingNote, true)) + return await this.sender.sendCmd('offer', p) + } + + async answer (bridgingNote, params) { + LOG.debug( + `rtpengine.RTPEngineConnector.answer [bridging note: ${bridgingNote}]` + ) + const p = merge(params, this.getBridgingInfo(bridgingNote, false)) + return await this.sender.sendCmd('answer', p) + } +} + +module.exports = RTPEngineConnector diff --git a/mod/rtpengine/ng_http_sender.js b/mod/rtpengine/ng_http_sender.js new file mode 100644 index 000000000..ec56a1a0d --- /dev/null +++ b/mod/rtpengine/ng_http_sender.js @@ -0,0 +1,42 @@ +const Unirest = Java.type('com.mashape.unirest.http.Unirest') +const LogManager = Java.type('org.apache.logging.log4j.LogManager') +const LOG = LogManager.getLogger() +const { benEncode, benDecode } = require('./utils') + +let cnt = 0 + +class NGHttpSender { + constructor (baseUrl) { + this.baseUrl = baseUrl + } + + async sendCmd (cmd, params) { + try { + LOG.debug( + `NGHttpSender.sendCmd cmd-${cmd} [call-id: ${params['call-id']}]` + ) + + params.command = cmd + + // The bencode id must be unique. So we add this value to avoid collition + cnt++ + + const res = await Unirest.post(`${this.baseUrl}`) + .header('Content-Type', 'application/x-rtpengine-ng') + .body(benEncode(params['call-id'] + cnt, params)) + .asString() + + LOG.debug( + `NGHttpSender.sendCmd cmd-${cmd} [results ${JSON.stringify( + benDecode(res.getBody()) + )}]` + ) + + return benDecode(res.getBody()).data + } catch (e) { + LOG.error(e) + } + } +} + +module.exports = NGHttpSender diff --git a/mod/rtpengine/rtp_bridging_note.js b/mod/rtpengine/rtp_bridging_note.js new file mode 100644 index 000000000..e7b6312c7 --- /dev/null +++ b/mod/rtpengine/rtp_bridging_note.js @@ -0,0 +1,10 @@ +/** + * @author Pedro Sanders + * @since v1 + */ +module.exports.RTPBridgingNote = { + WEB_TO_WEB: 'WEB_TO_WEB', + WEB_TO_SIP: 'WEB_TO_SIP', + SIP_TO_SIP: 'SIP_TO_SIP', + SIP_TO_WEB: 'SIP_TO_WEB' +} diff --git a/mod/rtpengine/utils.js b/mod/rtpengine/utils.js new file mode 100644 index 000000000..014c25aa5 --- /dev/null +++ b/mod/rtpengine/utils.js @@ -0,0 +1,74 @@ +/** + * @author Pedro Sanders + * @since v1 + */ +const { RTPBridgingNote } = require('@routr/rtpengine/rtp_bridging_note') +const ViaHeader = Java.type('javax.sip.header.ViaHeader') +const bencode = require('bencode') +const isTransportWeb = t => t === 'ws' || t === 'wss' +const LogManager = Java.type('org.apache.logging.log4j.LogManager') +const LOG = LogManager.getLogger() + +module.exports.directionFromRequest = (request, route) => { + const destTransport = route.transport.toLowerCase() + const srcTransport = request + .getHeader(ViaHeader.NAME) + .getTransport() + .toLowerCase() + + if (isTransportWeb(srcTransport) && isTransportWeb(destTransport)) + return RTPBridgingNote.WEB_TO_WEB + + if (isTransportWeb(srcTransport) && !isTransportWeb(destTransport)) + return RTPBridgingNote.WEB_TO_SIP + + if (!isTransportWeb(srcTransport) && !isTransportWeb(destTransport)) + return RTPBridgingNote.SIP_TO_SIP + + if (!isTransportWeb(srcTransport) && isTransportWeb(destTransport)) + return RTPBridgingNote.SIP_TO_WEB +} + +module.exports.directionFromResponse = response => { + const viaHeaders = response.getHeaders(ViaHeader.NAME) + if (viaHeaders.hasNext()) { + const viaHeaders = response.getHeaders(ViaHeader.NAME) + const srcTransport = viaHeaders + .next() + .getTransport() + .toLowerCase() + const destTransport = viaHeaders + .next() + .getTransport() + .toLowerCase() + + if (isTransportWeb(srcTransport) && isTransportWeb(destTransport)) + return RTPBridgingNote.WEB_TO_WEB + + if (isTransportWeb(srcTransport) && !isTransportWeb(destTransport)) + return RTPBridgingNote.SIP_TO_WEB + + if (!isTransportWeb(srcTransport) && isTransportWeb(destTransport)) + return RTPBridgingNote.WEB_TO_SIP + } + + return RTPBridgingNote.SIP_TO_SIP +} + +module.exports.benDecode = msg => { + const m = msg.toString() + const idx = m.indexOf(' ') + + if (-1 !== idx) { + const id = m.substring(0, idx) + try { + const data = bencode.decode(Buffer.from(m.substring(idx + 1)), 'utf-8') + return { id, data } + } catch (err) { + LOG.error(err) + } + } +} + +module.exports.benEncode = (id, data) => + Buffer.from([id, bencode.encode(data)].join(' ')) diff --git a/package.json b/package.json index 9404194ad..e17f70b79 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ } }, "dependencies": { + "bencode": "^2.0.1", "deepmerge": "^4.2.2", "flat": "^5.0.0", "ip-utils": "^2.4.0", @@ -37,20 +38,20 @@ "xxhashjs": "^0.2.2" }, "devDependencies": { - "source-map-loader": "^0.2.4", - "awesome-typescript-loader": "^5.2.1", "@types/node": "^13.13.2", + "awesome-typescript-loader": "^5.2.1", + "chai": "^4.2.0", "cross-env": "^5.2.0", "husky": "^4.2.3", "jvm-npm": "^0.1.1", "mocha": "^6.2.0", "nyc": "^14.1.1", "prettier-standard": "^9.1.1", - "chai": "^4.2.0", - "sinon-chai": "^3.5.0", "rewire": "^5.0.0", "sinon": "^9.0.1", - "webpack": "^4.11.1", + "sinon-chai": "^3.5.0", + "source-map-loader": "^0.2.4", + "webpack": "4.11.1", "webpack-command": "^0.2.0" }, "repository": {