Initial commit (Chrome PoC, tested on Vivaldi v7.0.3495.6)
This commit is contained in:
commit
860fa8ad90
|
@ -0,0 +1,243 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/node,vim,emacs,webstorm
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,vim,emacs,webstorm
|
||||
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
### WebStorm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,vim,emacs,webstorm
|
|
@ -0,0 +1,45 @@
|
|||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var favicon = require('serve-favicon');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var keysRouter = require('./routes/keys');
|
||||
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(favicon(path.join(__dirname,'public', 'images', 'favicon.ico')));
|
||||
app.use('/js/socket.io', express.static(path.join(
|
||||
__dirname, 'node_modules', 'socket.io','client-dist')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/keys', keysRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
module.exports = app;
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('webrtc-poc:server');
|
||||
var http = require('http');
|
||||
var socketio = require('socket.io');
|
||||
var Turn = require('node-turn');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
var io = socketio(server);
|
||||
|
||||
/**
|
||||
* Handle signaling events
|
||||
*/
|
||||
io.on('connection', socket => {
|
||||
console.log('Socket connected:', socket.id, 'at',
|
||||
socket.request.connection.remoteAddress);
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Socket disconnected:', socket.id, 'at', socket.request.connection.remoteAddress)
|
||||
});
|
||||
// Broadcast the client's offer
|
||||
socket.on('join-offer', offer => {
|
||||
console.log('Broadcast join-offer event from', socket.id);
|
||||
socket.broadcast.emit('join-offer', offer);
|
||||
});
|
||||
socket.on('join-answer', answer => {
|
||||
let dstSocket = JSON.parse(answer).dst;
|
||||
console.log('Relaying join-answer event from', socket.id, 'to', dstSocket);
|
||||
io.sockets.sockets.get(dstSocket).emit('join-answer', answer);
|
||||
});
|
||||
socket.on('join', info => {
|
||||
console.log('Broadcast join event from', socket.id);
|
||||
socket.broadcast.emit('join', info);
|
||||
});
|
||||
socket.on('leave', id => {
|
||||
socket.broadcast.emit('leave', id);
|
||||
});
|
||||
});
|
||||
|
||||
var turn_server = new Turn();
|
||||
turn_server.start();
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "webrtc-poc",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"morgan": "~1.9.1",
|
||||
"node-turn": "^0.0.6",
|
||||
"pug": "2.0.0-beta11",
|
||||
"serve-favicon": "^2.5.0",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -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}`)
|
||||
);
|
|
@ -0,0 +1,9 @@
|
|||
let express = require('express');
|
||||
let router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'WebRTC Auth PoC' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,43 @@
|
|||
let express = require('express');
|
||||
let router = express.Router();
|
||||
let keymap = {}
|
||||
|
||||
router.get('/:id', (req, res) => {
|
||||
let key = req.params.id.toLowerCase();
|
||||
if (key in keymap) {
|
||||
if (keymap[key] < Date.now()) {
|
||||
delete keymap[key];
|
||||
res.status(403).json({status: 'Key Expired'});
|
||||
} else {
|
||||
res.json({status: 'OK'});
|
||||
}
|
||||
} else {
|
||||
res.status(404).json({status: 'Not found'});
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
if (!('key' in req.body && 'expires' in req.body)) {
|
||||
res.status(400).json({status: 'Bad Request'});
|
||||
}
|
||||
let key = req.body.key.toLowerCase();
|
||||
let expires = req.body.expires;
|
||||
if (key in keymap) {
|
||||
res.status(400).json({status: 'Key exists'});
|
||||
} else {
|
||||
keymap[key] = expires
|
||||
res.json({status: 'OK'});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
let key = req.params.id.toLowerCase();
|
||||
if (key in keymap) {
|
||||
delete keymap[key];
|
||||
res.json({status: 'OK'});
|
||||
} else {
|
||||
res.status(404).json({status: 'Not found'});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,6 @@
|
|||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
|
@ -0,0 +1,25 @@
|
|||
extends layout
|
||||
|
||||
block content
|
||||
script(src='https://webrtc.github.io/adapter/adapter-latest.js' defer)
|
||||
script(src='js/socket.io/socket.io.js' defer)
|
||||
script(src='js/main.js' defer)
|
||||
h1= title
|
||||
h2 Certificate Status:#{' '}
|
||||
span(id='certStatus') Unknown
|
||||
div
|
||||
button(id='certLoad' disabled=true) Load
|
||||
button(id='certSave' disabled=true) Save
|
||||
button(id='certNew' disabled=true) New
|
||||
button(id='certUpload' disabled=true) Upload
|
||||
button(id='certClear' disabled=true) Clear
|
||||
h2 Chat Status:#{' '}
|
||||
span(id='chatStatus') Disconnected
|
||||
div
|
||||
button(id='chatJoin' disabled=true) Join
|
||||
button(id='chatLeave' disabled=true) Leave
|
||||
h2 Chat Log
|
||||
div
|
||||
input(id='chatInput')
|
||||
button(id='chatPost' disabled=true) Post
|
||||
div(id='chatLog')
|
|
@ -0,0 +1,7 @@
|
|||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='/css/style.css')
|
||||
body
|
||||
block content
|
Loading…
Reference in New Issue