diff --git a/core/main/client/lib/webrtcadapter.js b/core/main/client/lib/webrtcadapter.js index b14618849..6ecb0798c 100644 --- a/core/main/client/lib/webrtcadapter.js +++ b/core/main/client/lib/webrtcadapter.js @@ -1,190 +1,327 @@ -var RTCPeerConnection = null; +/* + * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + +/* More information about these options at jshint.com/docs/options */ +/* jshint browser: true, camelcase: true, curly: true, devel: true, + eqeqeq: true, forin: false, globalstrict: true, node: true, + quotmark: single, undef: true, unused: strict */ +/* global mozRTCIceCandidate, mozRTCPeerConnection, Promise, +mozRTCSessionDescription, webkitRTCPeerConnection, MediaStreamTrack */ +/* exported trace,requestUserMedia */ + +'use strict'; + var getUserMedia = null; var attachMediaStream = null; var reattachMediaStream = null; var webrtcDetectedBrowser = null; var webrtcDetectedVersion = null; +var webrtcMinimumVersion = null; -function maybeFixConfiguration(pcConfig) { - if (pcConfig === null) { - return; +function trace(text) { + // This function is used for logging. + if (text[text.length - 1] === '\n') { + text = text.substring(0, text.length - 1); } - for (var i = 0; i < pcConfig.iceServers.length; i++) { - if (pcConfig.iceServers[i].hasOwnProperty('urls')){ - if (pcConfig.iceServers[i]['urls'].length > 0) { - // In FF - we just take the FIRST STUN Server - pcConfig.iceServers[i]['url'] = pcConfig.iceServers[i]['urls'][0]; - } else { - pcConfig.iceServers[i]['url'] = pcConfig.iceServers[i]['urls']; - } - delete pcConfig.iceServers[i]['urls']; - } + if (window.performance) { + var now = (window.performance.now() / 1000).toFixed(3); + beef.debug(now + ': ' + text); + } else { + beef.debug(text); } } if (navigator.mozGetUserMedia) { - webrtcDetectedBrowser = "firefox"; + webrtcDetectedBrowser = 'firefox'; + // the detected firefox version. webrtcDetectedVersion = - parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); + parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); + + // the minimum firefox version still supported by adapter. + webrtcMinimumVersion = 31; // The RTCPeerConnection object. - var RTCPeerConnection = function(pcConfig, pcConstraints) { - // .urls is not supported in FF yet. - maybeFixConfiguration(pcConfig); - return new mozRTCPeerConnection(pcConfig, pcConstraints); - } - - // The RTCSessionDescription object. - RTCSessionDescription = mozRTCSessionDescription; - - // The RTCIceCandidate object. - RTCIceCandidate = mozRTCIceCandidate; - - // Get UserMedia (only difference is the prefix). - // Code from Adam Barth. - getUserMedia = navigator.mozGetUserMedia.bind(navigator); - navigator.getUserMedia = getUserMedia; - - // Creates iceServer from the url for FF. - createIceServer = function(url, username, password) { - var iceServer = null; - var url_parts = url.split(':'); - if (url_parts[0].indexOf('stun') === 0) { - // Create iceServer with stun url. - iceServer = { 'url': url }; - } else if (url_parts[0].indexOf('turn') === 0) { - if (webrtcDetectedVersion < 27) { - // Create iceServer with turn url. - // Ignore the transport parameter from TURN url for FF version <=27. - var turn_url_parts = url.split("?"); - // Return null for createIceServer if transport=tcp. - if (turn_url_parts.length === 1 || - turn_url_parts[1].indexOf('transport=udp') === 0) { - iceServer = {'url': turn_url_parts[0], - 'credential': password, - 'username': username}; + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (webrtcDetectedVersion < 38) { + // .urls is not supported in FF < 38. + // create RTCIceServers with a single url. + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (server.hasOwnProperty('urls')) { + for (var j = 0; j < server.urls.length; j++) { + var newServer = { + url: server.urls[j] + }; + if (server.urls[j].indexOf('turn') === 0) { + newServer.username = server.username; + newServer.credential = server.credential; + } + newIceServers.push(newServer); + } + } else { + newIceServers.push(pcConfig.iceServers[i]); + } } - } else { - // FF 27 and above supports transport parameters in TURN url, - // So passing in the full url to create iceServer. - iceServer = {'url': url, - 'credential': password, - 'username': username}; + pcConfig.iceServers = newIceServers; } } - return iceServer; + return new mozRTCPeerConnection(pcConfig, pcConstraints); }; - createIceServers = function(urls, username, password) { - var iceServers = []; - // Use .url for FireFox. - for (i = 0; i < urls.length; i++) { - var iceServer = createIceServer(urls[i], - username, - password); - if (iceServer !== null) { - iceServers.push(iceServer); - } - } - return iceServers; - } + // The RTCSessionDescription object. + window.RTCSessionDescription = mozRTCSessionDescription; + // The RTCIceCandidate object. + window.RTCIceCandidate = mozRTCIceCandidate; + + // getUserMedia constraints shim. + getUserMedia = (webrtcDetectedVersion < 38) ? + function(c, onSuccess, onError) { + var constraintsToFF37 = function(c) { + if (typeof c !== 'object' || c.require) { + return c; + } + var require = []; + Object.keys(c).forEach(function(key) { + var r = c[key] = (typeof c[key] === 'object') ? + c[key] : {ideal: c[key]}; + if (r.exact !== undefined) { + r.min = r.max = r.exact; + delete r.exact; + } + if (r.min !== undefined || r.max !== undefined) { + require.push(key); + } + if (r.ideal !== undefined) { + c.advanced = c.advanced || []; + var oc = {}; + oc[key] = {min: r.ideal, max: r.ideal}; + c.advanced.push(oc); + delete r.ideal; + if (!Object.keys(r).length) { + delete c[key]; + } + } + }); + if (require.length) { + c.require = require; + } + return c; + }; + beef.debug('spec: ' + JSON.stringify(c)); + c.audio = constraintsToFF37(c.audio); + c.video = constraintsToFF37(c.video); + beef.debug('ff37: ' + JSON.stringify(c)); + return navigator.mozGetUserMedia(c, onSuccess, onError); + } : navigator.mozGetUserMedia.bind(navigator); + + navigator.getUserMedia = getUserMedia; + + // Shim for mediaDevices on older versions. + if (!navigator.mediaDevices) { + navigator.mediaDevices = {getUserMedia: requestUserMedia, + addEventListener: function() { }, + removeEventListener: function() { } + }; + } + navigator.mediaDevices.enumerateDevices = + navigator.mediaDevices.enumerateDevices || function() { + return new Promise(function(resolve) { + var infos = [ + {kind: 'audioinput', deviceId: 'default', label:'', groupId:''}, + {kind: 'videoinput', deviceId: 'default', label:'', groupId:''} + ]; + resolve(infos); + }); + }; + + if (webrtcDetectedVersion < 41) { + // Work around http://bugzil.la/1169665 + var orgEnumerateDevices = + navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); + navigator.mediaDevices.enumerateDevices = function() { + return orgEnumerateDevices().catch(function(e) { + if (e.name === 'NotFoundError') { + return []; + } + throw e; + }); + }; + } // Attach a media stream to an element. attachMediaStream = function(element, stream) { - beef.debug("Attaching media stream"); + beef.debug('Attaching media stream'); element.mozSrcObject = stream; - element.play(); }; reattachMediaStream = function(to, from) { - beef.debug("Reattaching media stream"); + beef.debug('Reattaching media stream'); to.mozSrcObject = from.mozSrcObject; - to.play(); }; - // Fake get{Video,Audio}Tracks - if (!MediaStream.prototype.getVideoTracks) { - MediaStream.prototype.getVideoTracks = function() { - return []; - }; - } - - if (!MediaStream.prototype.getAudioTracks) { - MediaStream.prototype.getAudioTracks = function() { - return []; - }; - } } else if (navigator.webkitGetUserMedia) { - webrtcDetectedBrowser = "chrome"; - // Temporary fix until crbug/374263 is fixed. - // Setting Chrome version to 999, if version is unavailable. - var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - if (result !== null) { - webrtcDetectedVersion = parseInt(result[2], 10); - } else { - webrtcDetectedVersion = 999; - } + webrtcDetectedBrowser = 'chrome'; - // Creates iceServer from the url for Chrome M33 and earlier. - createIceServer = function(url, username, password) { - var iceServer = null; - var url_parts = url.split(':'); - if (url_parts[0].indexOf('stun') === 0) { - // Create iceServer with stun url. - iceServer = { 'url': url }; - } else if (url_parts[0].indexOf('turn') === 0) { - // Chrome M28 & above uses below TURN format. - iceServer = {'url': url, - 'credential': password, - 'username': username}; - } - return iceServer; - }; + // the detected chrome version. + webrtcDetectedVersion = + parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); - // Creates iceServers from the urls for Chrome M34 and above. - createIceServers = function(urls, username, password) { - var iceServers = []; - if (webrtcDetectedVersion >= 34) { - // .urls is supported since Chrome M34. - iceServers = {'urls': urls, - 'credential': password, - 'username': username }; - } else { - for (i = 0; i < urls.length; i++) { - var iceServer = createIceServer(urls[i], - username, - password); - if (iceServer !== null) { - iceServers.push(iceServer); - } - } - } - return iceServers; - }; + // the minimum chrome version still supported by adapter. + webrtcMinimumVersion = 38; // The RTCPeerConnection object. - var RTCPeerConnection = function(pcConfig, pcConstraints) { - // .urls is supported since Chrome M34. - if (webrtcDetectedVersion < 34) { - maybeFixConfiguration(pcConfig); - } - return new webkitRTCPeerConnection(pcConfig, pcConstraints); - } + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); + var origGetStats = pc.getStats.bind(pc); + pc.getStats = function(selector, successCallback, errorCallback) { // jshint ignore: line + // If selector is a function then we are in the old style stats so just + // pass back the original getStats format to avoid breaking old users. + if (typeof selector === 'function') { + return origGetStats(selector, successCallback); + } - // Get UserMedia (only difference is the prefix). - // Code from Adam Barth. - getUserMedia = navigator.webkitGetUserMedia.bind(navigator); + var fixChromeStats = function(response) { + var standardReport = {}; + var reports = response.result(); + reports.forEach(function(report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: report.type + }; + report.names().forEach(function(name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + + return standardReport; + }; + var successCallbackWrapper = function(response) { + successCallback(fixChromeStats(response)); + }; + return origGetStats(successCallbackWrapper, selector); + }; + + return pc; + }; + + // add promise support + ['createOffer', 'createAnswer'].forEach(function(method) { + var nativeMethod = webkitRTCPeerConnection.prototype[method]; + webkitRTCPeerConnection.prototype[method] = function() { + var self = this; + if (arguments.length < 1 || (arguments.length === 1 && + typeof(arguments[0]) === 'object')) { + var opts = arguments.length === 1 ? arguments[0] : undefined; + return new Promise(function(resolve, reject) { + nativeMethod.apply(self, [resolve, reject, opts]); + }); + } else { + return nativeMethod.apply(this, arguments); + } + }; + }); + + ['setLocalDescription', 'setRemoteDescription', + 'addIceCandidate'].forEach(function(method) { + var nativeMethod = webkitRTCPeerConnection.prototype[method]; + webkitRTCPeerConnection.prototype[method] = function() { + var args = arguments; + var self = this; + return new Promise(function(resolve, reject) { + nativeMethod.apply(self, [args[0], + function() { + resolve(); + if (args.length >= 2) { + args[1].apply(null, []); + } + }, + function(err) { + reject(err); + if (args.length >= 3) { + args[2].apply(null, [err]); + } + }] + ); + }); + }; + }); + + // getUserMedia constraints shim. + getUserMedia = function(c, onSuccess, onError) { + var constraintsToChrome = function(c) { + if (typeof c !== 'object' || c.mandatory || c.optional) { + return c; + } + var cc = {}; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced') { + return; + } + var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + var oldname = function(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return (name === 'deviceId') ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname('', key)] = r.exact; + } else { + ['min', 'max'].forEach(function(mix) { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + beef.debug('spec: ' + JSON.stringify(c)); // whitespace for alignment + c.audio = constraintsToChrome(c.audio); + c.video = constraintsToChrome(c.video); + beef.debug('chrome: ' + JSON.stringify(c)); + return navigator.webkitGetUserMedia(c, onSuccess, onError); + }; navigator.getUserMedia = getUserMedia; // Attach a media stream to an element. attachMediaStream = function(element, stream) { if (typeof element.srcObject !== 'undefined') { element.srcObject = stream; - } else if (typeof element.mozSrcObject !== 'undefined') { - element.mozSrcObject = stream; } else if (typeof element.src !== 'undefined') { element.src = URL.createObjectURL(stream); } else { @@ -195,6 +332,78 @@ if (navigator.mozGetUserMedia) { reattachMediaStream = function(to, from) { to.src = from.src; }; + + if (!navigator.mediaDevices) { + navigator.mediaDevices = {getUserMedia: requestUserMedia, + enumerateDevices: function() { + return new Promise(function(resolve) { + var kinds = {audio: 'audioinput', video: 'videoinput'}; + return MediaStreamTrack.getSources(function(devices) { + resolve(devices.map(function(device) { + return {label: device.label, + kind: kinds[device.kind], + deviceId: device.id, + groupId: ''}; + })); + }); + }); + }}; + // in case someone wants to listen for the devicechange event. + navigator.mediaDevices.addEventListener = function() { }; + navigator.mediaDevices.removeEventListener = function() { }; + } +} else if (navigator.mediaDevices && navigator.userAgent.match( + /Edge\/(\d+).(\d+)$/)) { + webrtcDetectedBrowser = 'edge'; + + webrtcDetectedVersion = + parseInt(navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)[2], 10); + + // the minimum version still supported by adapter. + webrtcMinimumVersion = 12; + + attachMediaStream = function(element, stream) { + element.srcObject = stream; + }; + reattachMediaStream = function(to, from) { + to.srcObject = from.srcObject; + }; } else { - beef.debug("Browser does not appear to be WebRTC-capable"); + // console.log('Browser does not appear to be WebRTC-capable'); +} + +// Returns the result of getUserMedia as a Promise. +function requestUserMedia(constraints) { + return new Promise(function(resolve, reject) { + getUserMedia(constraints, resolve, reject); + }); +} + +if (typeof module !== 'undefined') { + module.exports = { + RTCPeerConnection: window.RTCPeerConnection, + getUserMedia: getUserMedia, + attachMediaStream: attachMediaStream, + reattachMediaStream: reattachMediaStream, + webrtcDetectedBrowser: webrtcDetectedBrowser, + webrtcDetectedVersion: webrtcDetectedVersion, + webrtcMinimumVersion: webrtcMinimumVersion + //requestUserMedia: not exposed on purpose. + //trace: not exposed on purpose. + }; +} else if ((typeof require === 'function') && (typeof define === 'function')) { + // Expose objects and functions when RequireJS is doing the loading. + define([], function() { + return { + RTCPeerConnection: window.RTCPeerConnection, + getUserMedia: getUserMedia, + attachMediaStream: attachMediaStream, + reattachMediaStream: reattachMediaStream, + webrtcDetectedBrowser: webrtcDetectedBrowser, + webrtcDetectedVersion: webrtcDetectedVersion, + webrtcMinimumVersion: webrtcMinimumVersion + //requestUserMedia: not exposed on purpose. + //trace: not exposed on purpose. + }; + }); } diff --git a/core/main/client/webrtc.js b/core/main/client/webrtc.js index d8bb7a32a..db756cdaf 100644 --- a/core/main/client/webrtc.js +++ b/core/main/client/webrtc.js @@ -67,10 +67,12 @@ Beefwebrtc.prototype.initialize = function() { // Initialise the pcConfig hash with the provided stunservers var stuns = JSON.parse(this.stunservers); - this.pcConfig = {"iceServers": [{"urls":stuns}]}; + this.pcConfig = {"iceServers": [{"urls":stuns, "username":"user", + "credential":"pass"}]}; // We're not getting the browsers to request their own TURN servers, we're specifying them through BeEF - this.forceTurn(this.turnjson); + // this.forceTurn(this.turnjson); + this.turnDone = true; // Caller is always ready to create peerConnection. this.signalingReady = this.initiator; @@ -450,6 +452,18 @@ Beefwebrtc.prototype.onCreateSessionDescriptionError = function(error) { if (localverbose === true) {beef.debug('Failed to create session description: ' + error.toString());} } +// If the browser successfully sets a remote description +Beefwebrtc.prototype.onSetRemoteDescriptionSuccess = function() { + var localverbose = false; + + for (var k in beefrtcs) { + if (beefrtcs[k].verbose === true) { + localverbose = true; + } + } + if (localverbose === true) {beef.debug('Set remote session description successfully');} +} + // Check for messages - which includes signaling from a calling peer - this gets kicked off in maybeStart() Beefwebrtc.prototype.calleeStart = function() { // Callee starts to process cached offer and other messages. @@ -467,11 +481,55 @@ Beefwebrtc.prototype.processSignalingMessage = function(message) { if (message.type === 'offer') { if (this.verbose) {beef.debug("Processing signalling message: OFFER");} - this.setRemote(message); - this.doAnswer(); + if (navigator.mozGetUserMedia) { // Mozilla shim fuckn shit - since the new + // version of FF - which no longer works + if (this.verbose) {beef.debug("Moz shim here");} + globalrtc[this.peerid].setRemoteDescription( + new RTCSessionDescription(message), + function() { + // globalrtc[this.peerid].createAnswer(function(answer) { + // globalrtc[this.peerid].setLocalDescription( + + var peerid = null; + + for (var k in beefrtcs) { + if (beefrtcs[k].allgood === false) { + peerid = beefrtcs[k].peerid; + } + } + + globalrtc[peerid].createAnswer(function(answer) { + globalrtc[peerid].setLocalDescription( + new RTCSessionDescription(answer), + function() { + beefrtcs[peerid].sendSignalMsg(answer); + },function(error) { + beef.debug("setLocalDescription error: " + error); + }); + },function(error) { + beef.debug("createAnswer error: " +error); + }); + },function(error) { + beef.debug("setRemoteDescription error: " + error); + }); + + } else { + this.setRemote(message); + this.doAnswer(); + } } else if (message.type === 'answer') { if (this.verbose) {beef.debug("Processing signalling message: ANSWER");} - this.setRemote(message); + if (navigator.mozGetUserMedia) { // terrible moz shim - as for the offer + if (this.verbose) {beef.debug("Moz shim here");} + globalrtc[this.peerid].setRemoteDescription( + new RTCSessionDescription(message), + function() {}, + function(error) { + beef.debug("setRemoteDescription error: " + error); + }); + } else { + this.setRemote(message); + } } else if (message.type === 'candidate') { if (this.verbose) {beef.debug("Processing signalling message: CANDIDATE");} var candidate = new RTCIceCandidate({sdpMLineIndex: message.label, @@ -486,11 +544,11 @@ Beefwebrtc.prototype.processSignalingMessage = function(message) { // Used to set the RTC remote session Beefwebrtc.prototype.setRemote = function(message) { globalrtc[this.peerid].setRemoteDescription(new RTCSessionDescription(message), - onSetRemoteDescriptionSuccess, this.onSetSessionDescriptionError); + this.onSetRemoteDescriptionSuccess, this.onSetSessionDescriptionError); - function onSetRemoteDescriptionSuccess() { - if (this.verbose) {beef.debug("Set remote session description success.");} - } + // function onSetRemoteDescriptionSuccess() { + // if (this.verbose) {beef.debug("Set remote session description success.");} + // } } // As part of the processSignalingMessage function, we check for 'offers' from peers. If there's an offer, we answer, as below @@ -585,4 +643,4 @@ beef.webrtc = { } } } -beef.regCmp('beef.webrtc'); \ No newline at end of file +beef.regCmp('beef.webrtc'); diff --git a/extensions/webrtc/config.yaml b/extensions/webrtc/config.yaml index c467534ea..391c7b165 100644 --- a/extensions/webrtc/config.yaml +++ b/extensions/webrtc/config.yaml @@ -9,6 +9,6 @@ beef: name: 'WebRTC' enable: false authors: ["xntrik"] - stunservers: '["stun:stun.l.google.com:19302","stun:stun1.l.google.com:19302"]' + stunservers: '["stun:stun.l.google.com:19302","stun:stun1.l.google.com:19302","turn:numb.viagenie.ca:3478"]' # stunservers: '["stun:stun.l.google.com:19302"]' - turnservers: '{"username": "someone%40somewhere.com", "password": "somepass", "uris": ["turn:numb.viagenie.ca:3478?transport=udp","turn:numb.viagenie.ca:3478?transport=tcp"]}' \ No newline at end of file + turnservers: '{"username": "someone%40somewhere.com", "password": "somepass", "uris": ["turn:numb.viagenie.ca:3478?transport=udp","turn:numb.viagenie.ca:3478?transport=tcp"]}' diff --git a/extensions/webrtc/rest/webrtc.rb b/extensions/webrtc/rest/webrtc.rb index b7247d694..de68a17b9 100644 --- a/extensions/webrtc/rest/webrtc.rb +++ b/extensions/webrtc/rest/webrtc.rb @@ -152,7 +152,7 @@ module BeEF # +++ Example with curl +++ # curl -H "Content-type: application/json; charset=UTF-8" -v # -X POST -d '{"from":1,"to":2,"message":"Just a plain message"}' - # http://127.0.0.1:3000/api/webrtc/go\?token\=df67654b03d030d97018f85f0284247d7f49c348 + # http://127.0.0.1:3000/api/webrtc/msg\?token\=df67654b03d030d97018f85f0284247d7f49c348 # # Available client-side "message" options and handling: # !gostealth - will put the browser into a stealth mode diff --git a/test/common/test_constants.rb b/test/common/test_constants.rb index b994cbfc5..cf4950af7 100644 --- a/test/common/test_constants.rb +++ b/test/common/test_constants.rb @@ -23,3 +23,4 @@ RESTAPI_NETWORK = "http://" + ATTACK_DOMAIN + ":3000/api/network" RESTAPI_DNS = "http://" + ATTACK_DOMAIN + ":3000/api/dns" RESTAPI_SENG = "http://" + ATTACK_DOMAIN + ":3000/api/seng" RESTAPI_ADMIN = "http://" + ATTACK_DOMAIN + ":3000/api/admin" +RESTAPI_WEBRTC = "http://" + ATTACK_DOMAIN + ":3000/api/webrtc" diff --git a/test/integration/tc_webrtc_rest.rb b/test/integration/tc_webrtc_rest.rb new file mode 100644 index 000000000..69b6c4c71 --- /dev/null +++ b/test/integration/tc_webrtc_rest.rb @@ -0,0 +1,252 @@ +# +# Copyright (c) 2006-2015 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +require 'test/unit' +require 'rest-client' +require 'json' +require '../common/test_constants' +require '../common/beef_test' + +class TC_WebRTCRest < Test::Unit::TestCase + + class << self + + # Login to API before performing any tests - and fetch config too + def startup + json = {:username => BEEF_USER, :password => BEEF_PASSWD}.to_json + @@headers = {:content_type => :json, :accept => :json} + + response = RestClient.post("#{RESTAPI_ADMIN}/login", + json, + @@headers) + + result = JSON.parse(response.body) + @@token = result['token'] + + $root_dir = '../../' + $:.unshift($root_dir) + + require 'core/loader' + + BeEF::Core::Configuration.new(File.join($root_dir, 'config.yaml')) + BeEF::Core::Configuration.instance.load_extensions_config + + @@config = BeEF::Core::Configuration.instance + + @@activated = @@config.get('beef.extension.webrtc.enable') || false + + @@victim1 = BeefTest.new_victim + @@victim2 = BeefTest.new_victim + + puts "WebRTC Tests: Sleeping for 8 - waiting for 2 browsers to get hooked" + sleep 8.0 + + # Fetch last online browsers' ids + rest_response = RestClient.get "#{RESTAPI_HOOKS}", {:params => { + :token => @@token}} + result = JSON.parse(rest_response.body) + browsers = result["hooked-browsers"]["online"] + browsers.each_with_index do |elem, index| + if index == browsers.length - 1 + @@victim2id = browsers["#{index}"]["id"].to_s + end + if index == browsers.length - 2 + @@victim1id = browsers["#{index}"]["id"].to_s + end + end + + end + + def shutdown + $root_dir = nil + @@victim1.driver.browser.close + @@victim2.driver.browser.close + end + + end + + def test_1_webrtc_check_for_two_hooked_browsers + return if not @@activated + + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.get "#{RESTAPI_HOOKS}", {:params => { + :token => @@token}} + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + browsers = result["hooked-browsers"]["online"] + assert_not_nil browsers + assert_operator browsers.length, :>=, 2 + end + + def test_2_webrtc_establishing_p2p + return if not @@activated + + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.post("#{RESTAPI_WEBRTC}/go?token=#{@@token}", + {:from => @@victim1id, :to => @@victim2id, :verbose => "true"}.to_json, + @@headers) + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + assert_equal true, result["success"] + + sleep 20.0 + + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.get "#{RESTAPI_LOGS}", {:params => { + :token => @@token}} + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + + loghitcount = 0 + result["logs"].reverse.each {|l| + # Using free-space matching mode /x below to wrap regex. + # therefore need to escape spaces I want to check, hence the \ + regex = Regexp.new(/Browser:(#{@@victim1id}|#{@@victim2id})\ received\ + message\ from\ Browser:(#{@@victim1id}|#{@@victim2id}) + :\ ICE\ Status:\ connected/x) + loghitcount += 1 if (not regex.match(l["event"]).nil?) and + (l["type"].to_s.eql?("WebRTC")) + } + assert_equal 2, loghitcount + end + + def test_3_webrtc_send_msg # assumes test 2 has run + return if not @@activated + + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.post("#{RESTAPI_WEBRTC}/msg?token=#{@@token}", + {:from => @@victim1id, :to => @@victim2id, + :message => "RTC test message"}.to_json, + @@headers) + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + assert_equal true, result["success"] + + sleep 10.0 + + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.get "#{RESTAPI_LOGS}", {:params => { + :token => @@token}} + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + + assert_block do + result["logs"].reverse.each {|l| + # Using free-space matching mode /x below to wrap regex. + # therefore need to escape spaces I want to check, hence the \ + regex = Regexp.new(/Browser:(#{@@victim1id}|#{@@victim2id})\ received\ + message\ from\ Browser: + (#{@@victim1id}|#{@@victim2id}) + :\ RTC\ test\ message/x) + return true if (not regex.match(l["event"]).nil?) and + (l["type"].to_s.eql?("WebRTC")) + } + end + end + + def test_4_webrtc_stealthmode # assumes test 2 has run + return if not @@activated + + # Test our two browsers are still online + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.get "#{RESTAPI_HOOKS}", {:params => { + :token => @@token}} + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + online = result["hooked-browsers"]["online"] + assert_block do + online.each {|hb| + return true if hb[1]["id"].eql?(@@victim1id) + } + end + assert_block do + online.each {|hb| + return true if hb[1]["id"].eql?(@@victim2id) + } + end + + + # Command one of the browsers to go stealth + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.post("#{RESTAPI_WEBRTC}/msg?token=#{@@token}", + {:from => @@victim1id, :to => @@victim2id, + :message => "!gostealth"}.to_json, + @@headers) + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + assert_equal true, result["success"] + + sleep 40.0 #Wait until that browser is offline. + + # Test that the browser is now offline + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.get "#{RESTAPI_HOOKS}", {:params => { + :token => @@token}} + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + offline = result["hooked-browsers"]["offline"] + assert_block do + offline.each {|hb| + return true if hb[1]["id"].eql?(@@victim2id) + } + end + + # Test that we can bring it back online (which implies comms are still ok) + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.post("#{RESTAPI_WEBRTC}/msg?token=#{@@token}", + {:from => @@victim1id, :to => @@victim2id, + :message => "!endstealth"}.to_json, + @@headers) + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + assert_equal true, result["success"] + + sleep 10.0 # Wait until browser comes back + + # Test that the browser is now online + rest_response = nil + assert_nothing_raised do + rest_response = RestClient.get "#{RESTAPI_HOOKS}", {:params => { + :token => @@token}} + end + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + online = result["hooked-browsers"]["online"] + assert_block do + online.each {|hb| + return true if hb[1]["id"].eql?(@@victim2id) + } + end + + + end + + private + + # Standard assertions for verifying response from RESTful API + def check_rest_response(response) + assert_not_nil(response.body) + assert_equal(200, response.code) + end + +end diff --git a/test/integration/ts_integration.rb b/test/integration/ts_integration.rb index d896adf7c..2dc3103e2 100644 --- a/test/integration/ts_integration.rb +++ b/test/integration/ts_integration.rb @@ -19,6 +19,7 @@ require './tc_jools' # Basic tests for jools require './tc_network_rest' # Basic tests for Network extension RESTful API interface #require './tc_dns_rest' # Basic tests for DNS RESTful API interface require './tc_social_engineering_rest' # Basic tests for social engineering RESTful API interface +require './tc_webrtc_rest' # Basic tests for WebRTC extension class TS_BeefIntegrationTests def self.suite @@ -31,6 +32,7 @@ class TS_BeefIntegrationTests suite << TC_NetworkRest.suite #suite << TC_DnsRest.suite suite << TC_SocialEngineeringRest.suite + suite << TC_WebRTCRest.suite return suite end