├── .gitignore ├── .npmignore ├── Browser-Recording-Helper.js ├── Concatenate-Recordings.js ├── MediaStreamRecorder.js ├── Nodejs-Recording-Handler.js ├── README.md ├── Write-Recordings-To-Disk.js ├── fake-keys ├── certificate.pem └── privatekey.pem ├── index.html ├── package.json ├── server.js └── uploads └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | 4 | # bower 5 | bower_components 6 | 7 | npm-debug.log 8 | *.tar.gz 9 | 10 | uploads 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | npm-debug.log 4 | *.tar.gz 5 | uploads -------------------------------------------------------------------------------- /Browser-Recording-Helper.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.webrtc-experiment.com/licence 3 | // Documentation - github.com/streamproc/MediaStreamRecorder 4 | var RecorderHelper = (function() { 5 | var socket; // socket.io 6 | var roomId; 7 | var userId; 8 | var UploadInterval = 5 * 1000; 9 | 10 | var mediaStreamRecorder; 11 | 12 | function initRecorder(mediaStream, video) { 13 | mediaStream.addEventListener('ended', function() { 14 | // RecorderHelper.StopRecording(); 15 | }, false); 16 | 17 | mediaStreamRecorder = new MediaStreamRecorder(mediaStream); 18 | 19 | mediaStreamRecorder.mimeType = 'video/webm'; 20 | 21 | mediaStreamRecorder.ondataavailable = function(blobs) { 22 | onDataAvailable(blobs); 23 | }; 24 | mediaStreamRecorder.start(UploadInterval); 25 | 26 | socket.on('complete', function(fileName) { 27 | RecorderHelper.OnComplete(fileName); 28 | }); 29 | 30 | socket.on('ffmpeg-progress', function(response) { 31 | RecorderHelper.OnProgress(response); 32 | }); 33 | } 34 | 35 | window.addEventListener('beforeunload', function(event) { 36 | if (mediaStreamRecorder) { 37 | mediaStreamRecorder.stop(); 38 | mediaStreamRecorder = null; 39 | } 40 | 41 | if (Object.keys(socketPendingMessages).length) { 42 | event.returnValue = 'Still some recording intervals are pending.'; 43 | } 44 | }, false); 45 | 46 | function onDataAvailable(blob) { 47 | getDataURL(blob, function(dataURL) { 48 | var data = { 49 | blob: blob, 50 | dataURL: dataURL 51 | }; 52 | 53 | postFiles(data); 54 | }); 55 | } 56 | 57 | var fileNameString; 58 | var index = 1; 59 | 60 | function postFiles(data) { 61 | var interval = index; 62 | 63 | fileName = fileNameString + '-' + index; 64 | 65 | index++; 66 | 67 | var files = { 68 | interval: interval, 69 | roomId: roomId || generatefileNameString(), 70 | userId: userId || generatefileNameString(), 71 | fileName: fileNameString 72 | }; 73 | 74 | files.data = { 75 | name: fileName + '.' + data.blob.type.split('/')[1], 76 | type: data.blob.type, 77 | contents: data.dataURL, 78 | interval: interval 79 | }; 80 | 81 | if (isSocketBusy) { 82 | socketPendingMessages[interval] = { 83 | files: files, 84 | emit: function() { 85 | isSocketBusy = true; 86 | 87 | console.info('emitting', interval); 88 | 89 | socket.emit('recording-message', JSON.stringify(files), function() { 90 | isSocketBusy = false; 91 | 92 | if (socketPendingMessages[interval + 1]) { 93 | socketPendingMessages[interval + 1].emit(); 94 | delete socketPendingMessages[interval + 1]; 95 | } else if(!mediaStreamRecorder) { 96 | socket.emit('stream-stopped'); 97 | } 98 | }); 99 | } 100 | }; 101 | return; 102 | } 103 | 104 | isSocketBusy = true; 105 | console.info('emitting', interval); 106 | 107 | socket.emit('recording-message', JSON.stringify(files), function() { 108 | isSocketBusy = false; 109 | 110 | console.info('emitting', interval); 111 | 112 | if (socketPendingMessages[interval + 1]) { 113 | socketPendingMessages[interval + 1].emit(); 114 | delete socketPendingMessages[interval + 1]; 115 | } else if(!mediaStreamRecorder) { 116 | socket.emit('stream-stopped'); 117 | } 118 | }); 119 | } 120 | 121 | var isSocketBusy = false; 122 | var socketPendingMessages = {}; 123 | 124 | function generatefileNameString() { 125 | if (window.crypto) { 126 | var a = window.crypto.getRandomValues(new Uint32Array(3)), 127 | token = ''; 128 | for (var i = 0, l = a.length; i < l; i++) token += a[i].toString(36); 129 | return token; 130 | } else { 131 | return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); 132 | } 133 | } 134 | 135 | function getDataURL(blob, callback) { 136 | if (!!window.Worker) { 137 | var webWorker = processInWebWorker(function readFile(_blob) { 138 | postMessage(new FileReaderSync().readAsDataURL(_blob)); 139 | }); 140 | 141 | webWorker.onmessage = function(event) { 142 | callback(event.data); 143 | }; 144 | 145 | webWorker.postMessage(blob); 146 | } else { 147 | var reader = new FileReader(); 148 | reader.readAsDataURL(blob); 149 | reader.onload = function(event) { 150 | callback(event.target.result); 151 | }; 152 | } 153 | } 154 | 155 | var worker; 156 | 157 | function processInWebWorker(_function) { 158 | if (worker) { 159 | return worker; 160 | } 161 | 162 | var blob = URL.createObjectURL(new Blob([_function.toString(), 163 | 'this.onmessage = function (e) {' + _function.name + '(e.data);}' 164 | ], { 165 | type: 'application/javascript' 166 | })); 167 | 168 | worker = new Worker(blob); 169 | URL.revokeObjectURL(blob); 170 | return worker; 171 | } 172 | 173 | return { 174 | StartRecording: function(obj) { 175 | index = 1; 176 | 177 | fileNameString = obj.FileName || generatefileNameString(); 178 | 179 | roomId = obj.roomId; 180 | userId = obj.userId; 181 | UploadInterval = obj.UploadInterval; 182 | 183 | socket = obj.Socket; 184 | 185 | initRecorder(obj.MediaStream, obj.HTMLVideoElement); 186 | 187 | this.alreadyStopped = false; 188 | }, 189 | 190 | StopRecording: function() { 191 | if (this.alreadyStopped) return; 192 | this.alreadyStopped = true; 193 | 194 | mediaStreamRecorder.stop(); 195 | mediaStreamRecorder = null; 196 | }, 197 | 198 | OnComplete: function(fileName) { 199 | console.debug('File saved at: /uploads/' + roomId + '/' + fileName); 200 | }, 201 | 202 | OnProgress: function(response) { 203 | console.info('ffmpeg progress', response.progress, response); 204 | } 205 | }; 206 | })(); 207 | -------------------------------------------------------------------------------- /Concatenate-Recordings.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.webrtc-experiment.com/licence 3 | // Documentation - github.com/streamproc/MediaStreamRecorder 4 | var exec = require('child_process').exec; 5 | var fs = require('fs'); 6 | var socket; 7 | 8 | module.exports = exports = function(files, _socket) { 9 | socket = _socket; 10 | 11 | console.log('concatenating files in room:', files.roomId, ' for user:', files.userId, ' at interval:', files.interval); 12 | 13 | concatenateInLinuxOrMac(files); 14 | }; 15 | 16 | var ffmpeg = require('fluent-ffmpeg'); 17 | 18 | function concatenateInLinuxOrMac(files) { 19 | var uploadsFolder = __dirname + '/uploads/' + files.roomId + '/'; 20 | 21 | var lastIndex = files.lastIndex; 22 | 23 | var allFiles = []; 24 | var isAnySingleFileStillInProgress = false; 25 | for (var i = 1; i < lastIndex; i++) { 26 | if (!fs.existsSync(uploadsFolder + files.fileName + '-' + i + ".webm")) { 27 | isAnySingleFileStillInProgress = true; 28 | i = lastIndex; 29 | break; 30 | } 31 | 32 | allFiles.push(uploadsFolder + files.fileName + '-' + i + '.webm'); 33 | } 34 | 35 | if (isAnySingleFileStillInProgress) { 36 | console.log('isAnySingleFileStillInProgress'); 37 | setTimeout(function() { 38 | concatenateInLinuxOrMac(files); 39 | }, 2000); 40 | return; 41 | } 42 | 43 | // ffmpeg -y -i video.webm -i screen.webm -filter_complex "[0:v]setpts=PTS-STARTPTS, pad=iw:ih[bg]; [1:v]scale=320:240,setpts=PTS-STARTPTS[fg]; [bg][fg]overlay=main_w-overlay_w-10:main_h-overlay_h-10" fullRecording.webm 44 | // ffmpeg -y -i 6354797637490482-1.webm -i 6354797637490482-2.webm fullRecording.webm 45 | 46 | console.log('executing ffmpeg command'); 47 | 48 | var ffmpegCommand = ffmpeg(allFiles[0]); 49 | allFiles.forEach(function(filePath, idx) { 50 | if (idx !== 0) { 51 | ffmpegCommand = ffmpegCommand.input(filePath); 52 | } 53 | }); 54 | 55 | ffmpegCommand.on('progress', function(progress) { 56 | socket.emit('ffmpeg-progress', { 57 | userId: files.userId, 58 | roomId: files.roomId, 59 | progress: progress 60 | }); 61 | }); 62 | 63 | ffmpegCommand.on('error', function(err) { 64 | console.log(err.message); 65 | }); 66 | 67 | ffmpegCommand.on('end', function() { 68 | console.log('Successfully concatenated all WebM files from recording interval ' + files.interval + '.'); 69 | socket.emit('complete', files.userId + '.webm'); 70 | 71 | unlink_merged_files(uploadsFolder + files.fileName, lastIndex); 72 | }); 73 | 74 | var final_file = uploadsFolder + files.userId + '.webm'; 75 | ffmpegCommand.mergeToFile(final_file, __dirname + '/temp-uploads/'); 76 | } 77 | 78 | // delete all files from specific user 79 | function unlink_merged_files(fileName, lastIndex, index) { 80 | console.log('unlinking redundant files'); 81 | 82 | function unlink_file(_index) { 83 | fs.unlink(fileName + '-' + _index + ".webm", function(error) { 84 | if (error) { 85 | setTimeout(function() { 86 | unlink_merged_files(fileName, lastIndex, _index); 87 | }, 5000); 88 | } 89 | }); 90 | } 91 | 92 | if (index) { 93 | unlink_file(index); 94 | return; 95 | } 96 | 97 | for (var i = 1; i < lastIndex; i++) { 98 | unlink_file(i); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /MediaStreamRecorder.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.webrtc-experiment.com/licence 3 | // Documentation - github.com/streamproc/MediaStreamRecorder 4 | // ______________________ 5 | // MediaStreamRecorder.js 6 | /** 7 | * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html 8 | * The MediaRecorder accepts a mediaStream as input source passed from UA. When recorder starts, 9 | * a MediaEncoder will be created and accept the mediaStream as input source. 10 | * Encoder will get the raw data by track data changes, encode it by selected MIME Type, then store the encoded in EncodedBufferCache object. 11 | * The encoded data will be extracted on every timeslice passed from Start function call or by RequestData function. 12 | * Thread model: 13 | * When the recorder starts, it creates a "Media Encoder" thread to read data from MediaEncoder object and store buffer in EncodedBufferCache object. 14 | * Also extract the encoded data and create blobs on every timeslice passed from start function or RequestData function called by UA. 15 | */ 16 | function MediaStreamRecorder(mediaStream) { 17 | // if user chosen only audio option; and he tried to pass MediaStream with 18 | // both audio and video tracks; 19 | // using a dirty workaround to generate audio-only stream so that we can get audio/ogg output. 20 | if (this.type === 'audio' && mediaStream.getVideoTracks && mediaStream.getVideoTracks().length && !navigator.mozGetUserMedia) { 21 | var context = new AudioContext(); 22 | var mediaStreamSource = context.createMediaStreamSource(mediaStream); 23 | 24 | var destination = context.createMediaStreamDestination(); 25 | mediaStreamSource.connect(destination); 26 | 27 | mediaStream = destination.stream; 28 | } 29 | 30 | // void start(optional long timeSlice) 31 | // timestamp to fire "ondataavailable" 32 | 33 | // starting a recording session; which will initiate "Reading Thread" 34 | // "Reading Thread" are used to prevent main-thread blocking scenarios 35 | this.start = function(mTimeSlice) { 36 | mTimeSlice = mTimeSlice || 1000; 37 | isStopRecording = false; 38 | 39 | mediaRecorder = new MediaRecorder(mediaStream); 40 | 41 | mediaRecorder.ondataavailable = function(e) { 42 | if (isStopRecording) { 43 | return; 44 | } 45 | 46 | if (isPaused) { 47 | setTimeout(startRecording, 500); 48 | return; 49 | } 50 | 51 | console.log('ondataavailable', e.data.type, e.data.size, e.data); 52 | // mediaRecorder.state === 'recording' means that media recorder is associated with "session" 53 | // mediaRecorder.state === 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. 54 | 55 | if (!e.data.size) { 56 | console.warn('Recording of', e.data.type, 'failed.'); 57 | return; 58 | } 59 | 60 | // at this stage, Firefox MediaRecorder API doesn't allow to choose the output mimeType format! 61 | var blob = new window.Blob([e.data], { 62 | type: e.data.type || self.mimeType || 'audio/ogg' // It specifies the container format as well as the audio and video capture formats. 63 | }); 64 | 65 | // Dispatching OnDataAvailable Handler 66 | self.ondataavailable(blob); 67 | }; 68 | 69 | mediaRecorder.onstop = function(error) { 70 | // for video recording on Firefox, it will be fired quickly. 71 | // because work on VideoFrameContainer is still in progress 72 | // https://wiki.mozilla.org/Gecko:MediaRecorder 73 | 74 | // self.onstop(error); 75 | }; 76 | 77 | // http://www.w3.org/TR/2012/WD-dom-20121206/#error-names-table 78 | // showBrowserSpecificIndicator: got neither video nor audio access 79 | // "VideoFrameContainer" can't be accessed directly; unable to find any wrapper using it. 80 | // that's why there is no video recording support on firefox 81 | 82 | // video recording fails because there is no encoder available there 83 | // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp#317 84 | 85 | // Maybe "Read Thread" doesn't fire video-track read notification; 86 | // that's why shutdown notification is received; and "Read Thread" is stopped. 87 | 88 | // https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html#error-handling 89 | mediaRecorder.onerror = function(error) { 90 | console.error(error); 91 | self.start(mTimeSlice); 92 | }; 93 | 94 | mediaRecorder.onwarning = function(warning) { 95 | console.warn(warning); 96 | }; 97 | 98 | // void start(optional long mTimeSlice) 99 | // The interval of passing encoded data from EncodedBufferCache to onDataAvailable 100 | // handler. "mTimeSlice < 0" means Session object does not push encoded data to 101 | // onDataAvailable, instead, it passive wait the client side pull encoded data 102 | // by calling requestData API. 103 | mediaRecorder.start(mTimeSlice); 104 | }; 105 | 106 | var isStopRecording = false; 107 | 108 | this.stop = function() { 109 | isStopRecording = true; 110 | 111 | if (self.onstop) { 112 | self.onstop({}); 113 | } 114 | }; 115 | 116 | var isPaused = false; 117 | 118 | this.pause = function() { 119 | if (!mediaRecorder) { 120 | return; 121 | } 122 | 123 | isPaused = true; 124 | 125 | if (mediaRecorder.state === 'recording') { 126 | mediaRecorder.pause(); 127 | } 128 | }; 129 | 130 | this.resume = function() { 131 | if (!mediaRecorder) { 132 | return; 133 | } 134 | 135 | isPaused = false; 136 | 137 | if (mediaRecorder.state === 'paused') { 138 | mediaRecorder.resume(); 139 | } 140 | }; 141 | 142 | this.ondataavailable = this.onstop = function() {}; 143 | 144 | // Reference to itself 145 | var self = this; 146 | 147 | if (!self.mimeType && !!mediaStream.getAudioTracks) { 148 | self.mimeType = mediaStream.getAudioTracks().length && mediaStream.getVideoTracks().length ? 'video/webm' : 'audio/ogg'; 149 | } 150 | 151 | // Reference to "MediaRecorderWrapper" object 152 | var mediaRecorder; 153 | } 154 | 155 | 156 | function bytesToSize(bytes) { 157 | var k = 1000; 158 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 159 | if (bytes === 0) { 160 | return '0 Bytes'; 161 | } 162 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); 163 | return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; 164 | } 165 | -------------------------------------------------------------------------------- /Nodejs-Recording-Handler.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.webrtc-experiment.com/licence 3 | // Documentation - github.com/streamproc/MediaStreamRecorder 4 | module.exports = exports = function(socket) { 5 | SocketExtender(socket); 6 | }; 7 | 8 | var mkdirp = require('mkdirp'); 9 | var fs = require('fs'); 10 | 11 | var WriteToDisk = require('./Write-Recordings-To-Disk.js'); 12 | var ConcatenateRecordings = require('./Concatenate-Recordings.js'); 13 | 14 | var roomsDirs = {}; 15 | 16 | function SocketExtender(socket) { 17 | var params = socket.handshake.query; 18 | 19 | function onGettingRecordedMessages(data, callback) { 20 | var file = JSON.parse(data); 21 | 22 | socket.roomId = file.roomId; 23 | socket.userId = file.userId; 24 | 25 | if (!roomsDirs[file.roomId]) { 26 | roomsDirs[file.roomId] = { 27 | usersIndexed: {} 28 | }; 29 | 30 | if (!fs.existsSync('./uploads/' + file.roomId)) { 31 | createNewDir('./uploads/' + file.roomId, data, onGettingRecordedMessages, callback); 32 | return; 33 | } 34 | 35 | onGettingRecordedMessages(data, callback); 36 | return; 37 | } 38 | 39 | if (!roomsDirs[file.roomId].usersIndexed[file.userId]) { 40 | roomsDirs[file.roomId].usersIndexed[file.userId] = { 41 | interval: file.interval, 42 | fileName: file.fileName 43 | }; 44 | } 45 | 46 | roomsDirs[file.roomId].usersIndexed[file.userId].interval = file.interval; 47 | 48 | console.log('writing file do disk', file.interval); 49 | 50 | WriteToDisk(file, socket); 51 | 52 | callback(); 53 | } 54 | 55 | socket.on('recording-message', onGettingRecordedMessages); 56 | socket.on('stream-stopped', onRecordingStopped); 57 | socket.on('disconnect', onRecordingStopped); 58 | 59 | function onRecordingStopped() { 60 | if (!socket.roomId || !socket.userId) return; 61 | 62 | console.log('onRecordingStopped'); 63 | 64 | if (!roomsDirs[socket.roomId] || !roomsDirs[socket.roomId].usersIndexed[socket.userId]) { 65 | console.log('skipped', socket.roomId, socket.userId); 66 | return; 67 | } 68 | 69 | var user = roomsDirs[socket.roomId].usersIndexed[socket.userId]; 70 | 71 | ConcatenateRecordings({ 72 | fileName: user.fileName, 73 | lastIndex: user.interval + 1, 74 | roomId: socket.roomId, 75 | userId: socket.userId, 76 | interval: user.interval 77 | }, socket); 78 | 79 | if (!!roomsDirs[socket.roomId] && !!roomsDirs[socket.roomId].usersIndexed[socket.userId]) { 80 | delete roomsDirs[socket.roomId].usersIndexed[socket.userId]; 81 | } 82 | 83 | if (!!roomsDirs[socket.roomId] && Object.keys(roomsDirs[socket.roomId].usersIndexed).length <= 1) { 84 | delete roomsDirs[socket.roomId]; 85 | } 86 | } 87 | 88 | } 89 | 90 | // FUNCTION used to create room-directory 91 | function createNewDir(path, data, onGettingRecordedMessages, callback) { 92 | mkdirp(path, function(err) { 93 | if (err) { 94 | return setTimeout(createNewDir, 1000); 95 | } 96 | onGettingRecordedMessages(data, callback); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Record Entire Meeting using Pure JavaScript API! 2 | 3 | [](https://npmjs.org/package/record-entire-meeting) [](https://npmjs.org/package/record-entire-meeting) 4 | 5 | ``` 6 | npm install record-entire-meeting 7 | 8 | node server.js 9 | # https://127.0.0.1:9001/ 10 | # https://localhost:9001/ 11 | ``` 12 | 13 | This application runs top over `MediaStreamRecorder.js`: 14 | 15 | * https://github.com/streamproc/MediaStreamRecorder 16 | 17 | # Browser Support 18 | 19 | 1. Canary with `chrome://flags/#enable-experimental-web-platform-features` 20 | 2. Firefox 21 | 22 | # Goals 23 | 24 | * Record both audio/video from each user participating in a meeting room. 25 | * Record all videos from all the participants. 26 | * Merge/Mux then Concatenate using Ffmpeg on Node.js server 27 | * Scale videos at the end into a single grid-like stream so that later viewers are given single file containing all the videos and audios. 28 | 29 | # Use in your own applications 30 | 31 | ```javascript 32 | // 1st step 33 | var NodeJsRecordingHandler = require('./Nodejs-Recording-Handler.js'); 34 | 35 | io.on('connection', function(socket) { 36 | // 2nd & last step: 37 | // call below line for each socket connection 38 | // it will never affect your other socket.io events or objects 39 | NodeJsRecordingHandler(socket); 40 | 41 | // your custom socket.io code goes here 42 | }); 43 | ``` 44 | 45 | 46 | ## License 47 | 48 | [Record-Entire-Meeting](https://github.com/streamproc/Record-Entire-Meeting) is released under [MIT licence](https://www.webrtc-experiment.com/licence/). Copyright (c) [Muaz Khan](http://www.MuazKhan.com/). 49 | -------------------------------------------------------------------------------- /Write-Recordings-To-Disk.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.webrtc-experiment.com/licence 3 | // Documentation - github.com/streamproc/MediaStreamRecorder 4 | var fs = require('fs'); 5 | var socket; 6 | 7 | module.exports = exports = function(file, _socket) { 8 | socket = _socket; 9 | 10 | writeToDisk(file); 11 | }; 12 | 13 | function writeToDisk(file) { 14 | file.data.roomId = file.roomId; 15 | writeToDiskInternal(file.data, function() { 16 | 17 | }); 18 | } 19 | 20 | function writeToDiskInternal(file, callback) { 21 | var fileRootName = file.name.split('.').shift(), 22 | fileExtension = 'webm', 23 | filePathBase = './uploads/' + file.roomId + '/', 24 | fileRootNameWithBase = filePathBase + fileRootName, 25 | filePath = fileRootNameWithBase + '.' + fileExtension, 26 | fileID = 2, 27 | fileBuffer; 28 | 29 | if (fs.existsSync(filePath)) { 30 | fs.unlink(filePath); 31 | } 32 | 33 | file.contents = file.contents.split(',').pop(); 34 | 35 | fileBuffer = new Buffer(file.contents, "base64"); 36 | 37 | fs.writeFile(filePath, fileBuffer, function(error) { 38 | if (error) throw error; 39 | 40 | callback(); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /fake-keys/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICjTCCAfYCCQC8xCdh8aBfxDANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC 3 | VVMxEzARBgNVBAgTCldhc2hpbmd0b24xETAPBgNVBAcTCFJpY2hsYW5kMQ0wCwYD 4 | VQQKFAQmeWV0MQswCQYDVQQLFAImITEVMBMGA1UEAxMMTmF0aGFuIEZyaXR6MSAw 5 | HgYJKoZIhvcNAQkBFhFuYXRoYW5AYW5keWV0Lm5ldDAeFw0xMTEwMTkwNjI2Mzha 6 | Fw0xMTExMTgwNjI2MzhaMIGKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu 7 | Z3RvbjERMA8GA1UEBxMIUmljaGxhbmQxDTALBgNVBAoUBCZ5ZXQxCzAJBgNVBAsU 8 | AiYhMRUwEwYDVQQDEwxOYXRoYW4gRnJpdHoxIDAeBgkqhkiG9w0BCQEWEW5hdGhh 9 | bkBhbmR5ZXQubmV0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRKUxB1pLV 10 | RQ/dcUEP1p1oTIg0GoEvMPl6s7kC2Mroyovn/FaCzsgvwYhuwIeA6qgYoNIkSkXM 11 | QRmtfTpBvJNqM6A7jpUUmYuaUgqdrh5GZ5FGJjgAGIRJBWtovqxnCaHcmBYxlj0o 12 | /nxDmzgK655WBso7nwpixrzbsV3x7ZG45QIDAQABMA0GCSqGSIb3DQEBBQUAA4GB 13 | ALeMY0Og6SfSNXzvATyR1BYSjJCG19AwR/vafK4vB6ejta37TGEPOM66BdtxH8J7 14 | T3QuMki9Eqid0zPATOttTlAhBeDGzPOzD4ohJu55PwY0jTJ2+qFUiDKmmCuaUbC6 15 | JCt3LWcZMvkkMfsk1HgyUEKat/Lrs/iaVU6TDMFa52v5 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /fake-keys/privatekey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDRKUxB1pLVRQ/dcUEP1p1oTIg0GoEvMPl6s7kC2Mroyovn/FaC 3 | zsgvwYhuwIeA6qgYoNIkSkXMQRmtfTpBvJNqM6A7jpUUmYuaUgqdrh5GZ5FGJjgA 4 | GIRJBWtovqxnCaHcmBYxlj0o/nxDmzgK655WBso7nwpixrzbsV3x7ZG45QIDAQAB 5 | AoGBAJQzUtkDlJ6QhKE+8f6q7nVMZOWmMgqiBOMwHNMrkPpJKcCCRzoAEk/kLSts 6 | N5bcraZlrQARsEr9hZgrtu+FEl1ROdKc6B3bJ5B6FigwY7m8/Z3+YdgwqV6NJGQk 7 | 3twY4PoJEdeZ7GX2QnX8RDjyFvLaZ12jiDic30Nrn1gwvOCxAkEA9Dp5r9yg4DT/ 8 | V4SE5+NPCJmeV7zwfW3XUQHWD4TaFbOCjnjWB/BnrrjXhvd3VNzwajrJvqq/UiM4 9 | bAG4VLz0CwJBANs+IYm3tYfeP5YsYJVMOJ5TcOAZ3T9jMF+QC9/ObwepW4D1woRr 10 | rCYxe01JyZpqqWnfeIUoJ70QL9uP8AgTrM8CQFFqGNymKL71C9XJ6GBA5zzPsPhA 11 | lM7LSgbIHOrJd8XaNIB4CalV28pj9f0ZC5+vkzlmZZB47RRdh1aB8EfXQWcCQGa8 12 | KI8WLNRsCrPeO6v6OZXHV99Lf2eSnTpKj6XiYBjg/WXiw7G1mseS7Ep9RyE61gQs 13 | mZccB/MKQMLMIhhGz/UCQQDog5KBVaMhwrw1hwZ5gDyZs2YrE75522BnAU1ajQj+ 14 | VmTkcBwCtfnbXsWcHnYQnLlvz2Bi9ov2JncmJ5F1kiIw 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |