Initial commit (Chrome PoC, tested on Vivaldi v7.0.3495.6)
This commit is contained in:
commit
860fa8ad90
13 changed files with 2904 additions and 0 deletions
8
public/css/style.css
Normal file
8
public/css/style.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
}
|
BIN
public/images/favicon.ico
Normal file
BIN
public/images/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
422
public/js/main.js
Normal file
422
public/js/main.js
Normal file
|
@ -0,0 +1,422 @@
|
|||
'use strict';
|
||||
|
||||
// General Utilities
|
||||
function setButtonClickable(id, state) {
|
||||
document.getElementById(id).disabled = !state;
|
||||
}
|
||||
|
||||
function registerButtonActions() {
|
||||
document.getElementById('certLoad').onclick = onLoadCert;
|
||||
document.getElementById('certSave').onclick = onSaveCert;
|
||||
document.getElementById('certNew').onclick = onNewCert;
|
||||
document.getElementById('certUpload').onclick = onUploadCert;
|
||||
document.getElementById('certClear').onclick = onClearCert;
|
||||
document.getElementById('chatJoin').onclick = onJoinChat;
|
||||
document.getElementById('chatLeave').onclick = onLeaveChat;
|
||||
document.getElementById('chatPost').onclick = onPostChat;
|
||||
document.getElementById('chatInput').onkeydown = event => {
|
||||
if (document.getElementById('chatPost').disabled)
|
||||
return;
|
||||
if (event.key === 'Enter') {
|
||||
onPostChat();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========================== Certificate Management ==========================
|
||||
let certDB = null;
|
||||
let localCert = null;
|
||||
|
||||
// Utilities
|
||||
function getCertificateChecksum(cert) {
|
||||
let fp = cert.getFingerprints().reduce((res, curr) =>
|
||||
curr.algorithm === 'sha-256' ? curr : res, null);
|
||||
console.assert(fp !== null, 'Certificate does not have a sha256sum');
|
||||
return fp.value.split(':').join('');
|
||||
}
|
||||
|
||||
// IndexedDB
|
||||
function openCertDatabase(onSuccess, onError) {
|
||||
let req = window.indexedDB.open('webrtc-poc', 1);
|
||||
req.onupgradeneeded = () => {
|
||||
console.log('Initializing new IndexedDB');
|
||||
let db = req.result;
|
||||
let certStore = db.createObjectStore('dtlsCerts', {keyPath: 'id'});
|
||||
certStore.createIndex('by_id', 'id');
|
||||
};
|
||||
req.onsuccess = () => onSuccess(req.result);
|
||||
req.onerror = () => onError(req.error);
|
||||
}
|
||||
|
||||
function saveCertificate(db, cert, onSuccess, onError) {
|
||||
let certTx = db.transaction('dtlsCerts', 'readwrite');
|
||||
let certStore = certTx.objectStore('dtlsCerts');
|
||||
let certPut = certStore.put({
|
||||
// TODO: replace ID int with connection identifier
|
||||
id: 0,
|
||||
cert: cert,
|
||||
});
|
||||
certPut.onsuccess = onSuccess;
|
||||
certPut.onerror = () => onError(certPut.error);
|
||||
}
|
||||
|
||||
function loadCertificate(db, onSuccess, onError) {
|
||||
let certTx = db.transaction('dtlsCerts', 'readonly');
|
||||
let certStore = certTx.objectStore('dtlsCerts');
|
||||
// TODO: replace ID int with connection identifier
|
||||
let certGet = certStore.get(0)
|
||||
certGet.onsuccess = () => {
|
||||
let match = certGet.result;
|
||||
if (match !== undefined) {
|
||||
onSuccess(match.cert);
|
||||
} else {
|
||||
onSuccess(null);
|
||||
}
|
||||
};
|
||||
certGet.onerror = () => onError(certGet.error);
|
||||
}
|
||||
|
||||
// Cert Status Display
|
||||
function setCertStatusDisplay(status) {
|
||||
document.getElementById('certStatus').innerText = status;
|
||||
}
|
||||
|
||||
// Load Button
|
||||
function onLoadCert() {
|
||||
console.assert(certDB !== null, 'IndexedDB not available');
|
||||
loadCertificate(certDB,
|
||||
cert => {
|
||||
if (cert !== null) {
|
||||
console.log('Loaded certificate:', getCertificateChecksum(cert));
|
||||
console.log('Expires:', cert.expires);
|
||||
localCert = cert;
|
||||
setCertStatusDisplay('Loaded');
|
||||
setButtonClickable('certUpload', true);
|
||||
setButtonClickable('certClear', true);
|
||||
setButtonClickable('certSave', true);
|
||||
setButtonClickable('chatJoin', true);
|
||||
} else {
|
||||
setCertStatusDisplay('Load Failed (No Certificate Found)');
|
||||
}
|
||||
},
|
||||
err => {
|
||||
setCertStatusDisplay(`Load Failed (${err})`);
|
||||
},
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// Save Button
|
||||
function onSaveCert() {
|
||||
console.assert(localCert !== null, 'No local certificate available');
|
||||
console.assert(certDB !== null, 'IndexedDB not available');
|
||||
saveCertificate(certDB, localCert,
|
||||
() => {
|
||||
setCertStatusDisplay('Saved');
|
||||
},
|
||||
err => {
|
||||
setCertStatusDisplay(`Save Failed (${err})`);
|
||||
},
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// New Button
|
||||
function onNewCert() {
|
||||
RTCPeerConnection.generateCertificate({
|
||||
name: 'ECDSA',
|
||||
hash: 'SHA-256',
|
||||
namedCurve: 'P-256',
|
||||
}).then(cert => {
|
||||
console.log('Generated new certificate:', getCertificateChecksum(cert));
|
||||
console.log('Expires:', cert.expires);
|
||||
localCert = cert;
|
||||
setCertStatusDisplay('Available');
|
||||
setButtonClickable('certUpload', true);
|
||||
setButtonClickable('certClear', true);
|
||||
setButtonClickable('certSave', true);
|
||||
setButtonClickable('chatJoin', true);
|
||||
});
|
||||
}
|
||||
|
||||
// Upload Button
|
||||
function onUploadCert() {
|
||||
console.assert(localCert !== null, 'No local certificate available');
|
||||
fetch('/keys', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'key': getCertificateChecksum(localCert),
|
||||
'expires': localCert.expires,
|
||||
}),
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
setCertStatusDisplay('Uploaded');
|
||||
} else {
|
||||
res.json().then(res => {
|
||||
setCertStatusDisplay(`Upload Failed (${res.status})`);
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear Button
|
||||
function onClearCert() {
|
||||
console.assert(localCert !== null, 'No local certificate available');
|
||||
let fp = getCertificateChecksum(localCert);
|
||||
fetch(`/keys/${fp}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
setCertStatusDisplay('Cleared');
|
||||
localCert = null;
|
||||
setButtonClickable('certUpload', false);
|
||||
setButtonClickable('certClear', false);
|
||||
setButtonClickable('certSave', false);
|
||||
setButtonClickable('chatJoin', false);
|
||||
} else {
|
||||
res.json().then(res => {
|
||||
setCertStatusDisplay(`Delete Failed (${res.status})`);
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================== Chat Control ===============================
|
||||
let socket = null;
|
||||
let localDesc = null;
|
||||
let remoteDesc = null;
|
||||
let activeConnection = null;
|
||||
let activeChannel = null;
|
||||
let deferredCandidates = [];
|
||||
|
||||
// Chat Status Display
|
||||
function setChatStatusDisplay(status) {
|
||||
document.getElementById('chatStatus').innerText = status;
|
||||
}
|
||||
|
||||
function getCertificateChecksumFromSDP(desc) {
|
||||
let fp = desc.sdp.split('\n').reduce((res, curr) =>
|
||||
curr.startsWith('a=fingerprint:sha-256 ') ? curr : res, null);
|
||||
console.assert(fp !== null, 'Description does not have a sha256sum');
|
||||
return fp.trim().split(' ')[1].split(':').join('').toLowerCase();
|
||||
}
|
||||
|
||||
function verifyFingerprint(desc) {
|
||||
let fp = getCertificateChecksumFromSDP(desc);
|
||||
fetch(`/keys/${fp}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.status !== 200) {
|
||||
console.log('Unauthorized certificate detected! Terminating connection...');
|
||||
res.json().then(res => setChatStatusDisplay(`Terminated (${res.status})`));
|
||||
appendChatLog('SYSTEM', 'Terminating unauthorized session.');
|
||||
onLeaveChat();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onPeerOfferAvailable(msg) {
|
||||
// Ignore subsequent requests
|
||||
if (remoteDesc !== null) return;
|
||||
console.log('Received WebRTC connection offer, sending answer...');
|
||||
|
||||
console.assert(localDesc !== null, 'No local description available');
|
||||
|
||||
activeChannel.close();
|
||||
activeChannel = null;
|
||||
|
||||
remoteDesc = JSON.parse(msg);
|
||||
verifyFingerprint(remoteDesc.desc);
|
||||
activeConnection.setRemoteDescription(remoteDesc.desc).then(() =>
|
||||
activeConnection.createAnswer().then(answer => {
|
||||
localDesc = {src: socket.id, dst: remoteDesc.src, desc: answer};
|
||||
activeConnection.setLocalDescription(localDesc.desc).then(() => {
|
||||
socket.emit('join-answer', JSON.stringify(localDesc));
|
||||
for (let candidate of deferredCandidates) {
|
||||
activeConnection.addIceCandidate(candidate);
|
||||
}
|
||||
deferredCandidates = []
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function onPeerAnswerAvailable(msg) {
|
||||
// Ignore subsequent responses
|
||||
let currDesc = JSON.parse(msg);
|
||||
if (remoteDesc !== null && currDesc.src !== remoteDesc.src) return;
|
||||
|
||||
console.log('Received WebRTC connection answer...');
|
||||
console.assert(localDesc !== null, 'No local description available');
|
||||
|
||||
remoteDesc = currDesc;
|
||||
verifyFingerprint(remoteDesc.desc);
|
||||
activeConnection.setLocalDescription(localDesc.desc).then(() =>
|
||||
activeConnection.setRemoteDescription(remoteDesc.desc).then(() => {
|
||||
for (let candidate of deferredCandidates) {
|
||||
activeConnection.addIceCandidate(candidate);
|
||||
}
|
||||
deferredCandidates = []
|
||||
}));
|
||||
}
|
||||
|
||||
function onPeerJoinAvailable(msg) {
|
||||
let candidate = JSON.parse(msg);
|
||||
if (candidate.src === remoteDesc.src && candidate.dst === localDesc.src) {
|
||||
if (activeConnection.remoteDescription) {
|
||||
console.log('Received WebRTC ICE candidate info, registering...');
|
||||
activeConnection.addIceCandidate(candidate.candidate);
|
||||
} else {
|
||||
console.log('Received WebRTC ICE candidate info, deferring registration...');
|
||||
deferredCandidates.push(candidate.candidate);
|
||||
}
|
||||
} else if (candidate.src === remoteDesc.src) {
|
||||
onLeaveChat();
|
||||
onJoinChat();
|
||||
}
|
||||
}
|
||||
|
||||
function onPeerAvailable(event) {
|
||||
console.assert(remoteDesc !== null, 'No remote description available');
|
||||
if (event.candidate) {
|
||||
console.log('Sending WebRTC ICE candidate info...');
|
||||
socket.emit('join', JSON.stringify( {
|
||||
src: localDesc.src,
|
||||
dst: remoteDesc.src,
|
||||
candidate: event.candidate
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function registerChannelHandlers(channel) {
|
||||
channel.addEventListener('open', () => {
|
||||
setChatStatusDisplay('Connected');
|
||||
appendChatLog('SYSTEM', 'User joined the chat.');
|
||||
setButtonClickable('chatPost', true);
|
||||
channel.addEventListener('close', () => {
|
||||
appendChatLog('SYSTEM', 'User disconnected from the chat.');
|
||||
onLeaveChat();
|
||||
})
|
||||
});
|
||||
channel.addEventListener('message', event =>
|
||||
appendChatLog('Remote', event.data));
|
||||
}
|
||||
|
||||
function onRTCConnectionStateChange(event) {
|
||||
let connection = event.target
|
||||
let state = connection.connectionState;
|
||||
|
||||
console.log('WebRTC connection state update:', state);
|
||||
}
|
||||
|
||||
// Join Button
|
||||
function onJoinChat() {
|
||||
console.assert(localCert !== null, 'No local certificate available');
|
||||
console.assert(activeConnection === null, 'Local connection exists');
|
||||
console.assert(activeChannel === null, 'Local channel exists');
|
||||
|
||||
socket = io(window.location.protocol + '//' + window.location.host);
|
||||
socket.on('join-offer', onPeerOfferAvailable);
|
||||
socket.on('join-answer', onPeerAnswerAvailable);
|
||||
socket.on('join', onPeerJoinAvailable);
|
||||
socket.on('connect', () => {
|
||||
let rtcCfg = {
|
||||
iceServers: [{urls: `stun:${window.location.hostname}:3478`}],
|
||||
certificates: [localCert],
|
||||
};
|
||||
activeConnection = new RTCPeerConnection(rtcCfg);
|
||||
activeConnection.addEventListener('icecandidate', onPeerAvailable);
|
||||
activeConnection.addEventListener('connectionstatechange', onRTCConnectionStateChange);
|
||||
activeConnection.addEventListener('datachannel', event => {
|
||||
console.assert(activeChannel === null);
|
||||
activeChannel = event.channel;
|
||||
registerChannelHandlers(activeChannel)
|
||||
});
|
||||
activeChannel = activeConnection.createDataChannel('chatChannel');
|
||||
registerChannelHandlers(activeChannel);
|
||||
activeConnection.createOffer().then(offer => {
|
||||
localDesc = {src: socket.id, desc: offer};
|
||||
// Broadcast join offer
|
||||
console.log('Sending WebRTC connection offer...');
|
||||
socket.emit('join-offer', JSON.stringify(localDesc));
|
||||
})
|
||||
|
||||
setButtonClickable('chatJoin', false);
|
||||
setButtonClickable('chatLeave', true);
|
||||
setButtonClickable('certNew', false);
|
||||
setButtonClickable('certLoad', false);
|
||||
setButtonClickable('certClear', false);
|
||||
setChatStatusDisplay('Awaiting connection');
|
||||
clearChatLog();
|
||||
});
|
||||
}
|
||||
|
||||
// Leave Button
|
||||
function onLeaveChat() {
|
||||
if (socket !== null) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
if (activeChannel !== null) {
|
||||
activeChannel.close();
|
||||
activeChannel = null;
|
||||
}
|
||||
|
||||
if (activeConnection !== null) {
|
||||
activeConnection.close();
|
||||
activeConnection = null;
|
||||
}
|
||||
|
||||
localDesc = null;
|
||||
remoteDesc = null;
|
||||
|
||||
setButtonClickable('chatJoin', true);
|
||||
setButtonClickable('chatLeave', false);
|
||||
setButtonClickable('certNew', true);
|
||||
setButtonClickable('certLoad', true);
|
||||
setButtonClickable('certClear', true);
|
||||
setChatStatusDisplay('Disconnected');
|
||||
}
|
||||
|
||||
// ================================= Chat Log =================================
|
||||
function clearChatLog() {
|
||||
document.getElementById('chatLog').innerHTML = '';
|
||||
}
|
||||
|
||||
function appendChatLog(name, msg) {
|
||||
document.getElementById('chatLog').innerHTML += `<p>${name}: ${msg}</p>`;
|
||||
}
|
||||
|
||||
// Post Button
|
||||
function onPostChat() {
|
||||
let input = document.getElementById('chatInput');
|
||||
let text = input.value;
|
||||
appendChatLog('Local', text);
|
||||
activeChannel.send(text);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
registerButtonActions();
|
||||
openCertDatabase(
|
||||
db => {
|
||||
console.assert(certDB === null, 'IndexedDB already open');
|
||||
certDB = db;
|
||||
console.log('IndexedDB opened.');
|
||||
setButtonClickable('certNew', true);
|
||||
setButtonClickable('certLoad', true);
|
||||
},
|
||||
err => console.error(`IndexedDB open error: ${err}`)
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue