screen sharinggit add .!
authorken <ken@kengrimes.com>
Fri, 7 Jul 2017 01:21:49 +0000 (01:21 +0000)
committerken <ken@kengrimes.com>
Fri, 7 Jul 2017 01:21:49 +0000 (01:21 +0000)
client.js
host.js
main.js
package-lock.json [new file with mode: 0644]
router.js [new file with mode: 0644]
usage

index f38f3ab..9b58c5d 100644 (file)
--- a/client.js
+++ b/client.js
-const body = document.createElement('body')
-const root = document.createElement('div')
-document.title = "Strapp.io Client"
-const conf = {"iceServers": [{ "urls": "stun:stun.1.google.com:19302" }] }
-let dataChannel
-
-/* TODO: duplicate in both client.js and host.js */
-function getPublicKey() {
-  return new Promise( (resolve, reject) => {
-    /* Check local storage for public key */
-    if (!window.localStorage.getItem('public-key')) {
-      /* If doesn't exist, generate public and private key pair, store in
-      local storage */
-      crypto.subtle.generateKey(
-        { name:'RSA-OAEP',
-        modulusLength: 2048,
-        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
-        hash: {name: "SHA-256"}
-      },
-      true,
-      ['encrypt', 'decrypt']
-    ).then((keyPair) => {
-      /* TODO: Do we need to store the private key as well? */
-      crypto.subtle.exportKey('jwk', keyPair.publicKey)
-      .then((exportedKey) => {
-        window.localStorage.setItem('publicKey', exportedKey)
-        console.log('public key is' + window.localStorage.getItem('publicKey'))
-        resolve(exportedKey)
-      })
-
-    })
-  }
-  else {
-    resolve(window.localStorage.getItem('publicKey'))
-  }
-})
-
-}
-
-function postServer(url, data) {
-  const request = new XMLHttpRequest()
-  request.open('POST', url, true)
-  request.setRequestHeader('Content-Type', 'application/json' )
-  request.setRequestHeader('X-Strapp-Type', 'ice-candidate-submission')
-  request.send(data)
-}
-
-/* TODO: All this does is wrap a function in a promise. Allows pollServerForAnswer
-to call itself recursively with the same promise */
-function pollServer(url, clientPubKey, func) {
-  return new Promise((resolve, reject) => {
-    func(url, clientPubKey, resolve, reject )
-  })
-}
-
-/* Poll the server. Send get request, wait for timeout, send another request.
-Do this until...? Can be used for either reconnecting or waiting for answer*/
-function pollServerForAnswer(url, data, resolve, reject) {
-  const request = new XMLHttpRequest()
-  request.open('GET', url, true)
-  /* But there is no JSON? */
-  request.setRequestHeader('Content-Type', 'application/json' )
-  request.setRequestHeader('X-Strapp-Type', 'client-sdp-offer')
-  request.setRequestHeader('X-Client-Offer', JSON.stringify(data))
-  request.onreadystatechange = () => {
-    if (request.status === 200) {
-      if(request.readyState === 4) {
-        console.log('Client: Recieved Answer from Host')
-        console.log(request)
-        resolve(request.response)
-      }
-    }
-    else if (request.status === 504) {
-      console.log('timed out, resending')
-      pollServerForAnswer(url, data, resolve, reject)
-    }
-    else {
-      reject('server unhandled response of status ' + request.status)
-    }
-  }
-  request.send()
-}
-
-/* Poll server for ice candidates until ice is complete */
-function pollServerForICECandidate(cpc, url, pubKey) {
-  let intervalID = window.setInterval(() => {
-    if (cpc.iceConnectionState.localeCompare('connected') !== 0
-    && cpc.iceConnectionState.localeCompare('completed') !== 0) {
-      console.log('Client: Polling server begin for intervalID = ' + intervalID)
-      console.log('Client: Requesting ICE Candidates from server')
-      const request = new XMLHttpRequest()
-      request.open('GET', url, true)
-      request.setRequestHeader('Content-Type', 'application/json' )
-      request.setRequestHeader('X-Strapp-Type', 'ice-candidate-request')
-      request.setRequestHeader('X-client-pubkey', pubKey)
-      request.onreadystatechange = () => {
-        if (request.status === 200) {
-          if(request.readyState === 4) {
-            console.log('Client: Recieved ICE response from Host')
-            let response = JSON.parse(request.response)
-            switch(response['iceState']) {
-              case "a":
-                cpc.addIceCandidate(new RTCIceCandidate(response.ice))
-                break
-              case "g": /* Gathering so let interval keep polling */
-                break
-              case "c": /* host iceState == Complete, stop bugging it */
-                clearInterval(intervalID)
-                clearTimeout()
-                break
-              default:
-                console.log('Unhandled iceState in pollServerForICECandidate()' + response['iceState'])
-                break
-            }
-          }
-        }
-        else {
-          console.log('server unhandled response of status ' + request.status)
-          clearInterval(intervalID)
-        }
-      }
-      request.send()
-    }
-    else {
-      clearTimeout()
-      clearInterval(intervalID)
-    }
-  }, 5000)
-}
-
-/* Create and send offer -> Send ICE Candidates -> Poll for ICE Candidates */
-getPublicKey().then((cpk) => {
-  console.log('Client: Create and send offer')
-  const cpc = new RTCPeerConnection(conf)
-
-  cpc.oniceconnectionstatechange = () => {
-    console.log('iceConnectionState = ' + cpc.iceConnectionState)
-  }
-
-  cpc.onnegotiationneeded = () => {
-    console.log('negotiation needed!')
-    cpc.createOffer().then((offer) => {
-      return cpc.setLocalDescription(offer)
-    })
-    .then(() => {
-      console.log('Client: Sending offer to host')
-      let offer = {
-        cmd: '> sdp pubKey',
-        sdp: cpc.localDescription,
-        pubKey: cpk.n
-      }
-      return pollServer(window.location, offer, pollServerForAnswer)
-    }).then((serverResponse) => {
-      const answer = JSON.parse(serverResponse)
-      console.log('Client: Polling for ICE candidates')
-      pollServerForICECandidate(cpc, window.location, cpk.n)
-      cpc.setRemoteDescription(answer.sdp)
-      cpc.onicecandidate = (event) => {
-        if (event.candidate) {
-          console.log('Client: Sending ice candidate to host')
-          postServer(window.location, JSON.stringify({
-            cmd: '> ice pubkey',
-            ice: event.candidate,
-            pubKey: cpk.n
-          }))
-        }
-        else {
-          console.log('Client: No more Ice Candidates to send')
-        }
-      }
-
-
-    }).catch( (err) => {
-      console.log('error in sdp handshake: ' + err)
-    })
-  }
-  /* Start data channel */
-  dataChannel = cpc.createDataChannel("sendChannel");
-  dataChannel.onmessage = (msg) => {
-    /* Get mediaStream from host and add it to the video */
-    let video = document.querySelector('')
-  }
-  dataChannel.onopen = () => {
-    dataChannel.send(`Hi from the Client`)
-    document.write('<button> Connection with host established! </button> <video autoplay id="screenOutput"></video>')
-  }
-
-})
-document.addEventListener('DOMContentLoaded', () => {
-
-    document.body.innerHTML = `<button> Setting up connection with host </button>`
-
-});
+const body = document.createElement('body')\r
+const root = document.createElement('div')\r
+document.title = "Strapp.io Client"\r
+const conf = {"iceServers": [{ "urls": "stun:stun.1.google.com:19302" }] }\r
+let dataChannel\r
+let hostScreen\r
+\r
+/* TODO: duplicate in both client.js and host.js */\r
+function getPublicKey() {\r
+  return new Promise( (resolve, reject) => {\r
+    /* Check local storage for public key */\r
+    if (!window.localStorage.getItem('public-key')) {\r
+      /* If doesn't exist, generate public and private key pair, store in\r
+      local storage */\r
+      crypto.subtle.generateKey(\r
+        { name:'RSA-OAEP',\r
+        modulusLength: 2048,\r
+        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),\r
+        hash: {name: "SHA-256"}\r
+      },\r
+      true,\r
+      ['encrypt', 'decrypt']\r
+    ).then((keyPair) => {\r
+      /* TODO: Do we need to store the private key as well? */\r
+      crypto.subtle.exportKey('jwk', keyPair.publicKey)\r
+      .then((exportedKey) => {\r
+        window.localStorage.setItem('publicKey', exportedKey)\r
+        console.log('public key is' + window.localStorage.getItem('publicKey'))\r
+        resolve(exportedKey)\r
+      })\r
+\r
+    })\r
+  }\r
+  else {\r
+    resolve(window.localStorage.getItem('publicKey'))\r
+  }\r
+})\r
+\r
+}\r
+\r
+function postServer(url, data) {\r
+  const request = new XMLHttpRequest()\r
+  request.open('POST', url, true)\r
+  request.setRequestHeader('Content-Type', 'application/json' )\r
+  request.setRequestHeader('X-Strapp-Type', 'ice-candidate-submission')\r
+  request.send(data)\r
+}\r
+\r
+/* TODO: All this does is wrap a function in a promise. Allows pollServerForAnswer\r
+to call itself recursively with the same promise */\r
+function pollServer(url, clientPubKey, func) {\r
+  return new Promise((resolve, reject) => {\r
+    func(url, clientPubKey, resolve, reject )\r
+  })\r
+}\r
+\r
+/* Poll the server. Send get request, wait for timeout, send another request.\r
+Do this until...? Can be used for either reconnecting or waiting for answer*/\r
+function pollServerForAnswer(url, data, resolve, reject) {\r
+  const request = new XMLHttpRequest()\r
+  request.open('GET', url, true)\r
+  /* But there is no JSON? */\r
+  request.setRequestHeader('Content-Type', 'application/json' )\r
+  request.setRequestHeader('X-Strapp-Type', 'client-sdp-offer')\r
+  request.setRequestHeader('X-Client-Offer', JSON.stringify(data))\r
+  request.onreadystatechange = () => {\r
+    if (request.status === 200) {\r
+      if(request.readyState === 4) {\r
+        console.log('Client: Recieved Answer from Host')\r
+        console.log(request)\r
+        resolve(request.response)\r
+      }\r
+    }\r
+    else if (request.status === 504) {\r
+      console.log('timed out, resending')\r
+      pollServerForAnswer(url, data, resolve, reject)\r
+    }\r
+    else {\r
+      reject('server unhandled response of status ' + request.status)\r
+    }\r
+  }\r
+  request.send()\r
+}\r
+\r
+/* Poll server for ice candidates until ice is complete */\r
+function pollServerForICECandidate(cpc, url, pubKey) {\r
+  let intervalID = window.setInterval(() => {\r
+    if (cpc.iceConnectionState.localeCompare('connected') !== 0\r
+    && cpc.iceConnectionState.localeCompare('completed') !== 0) {\r
+      console.log('Client: Polling server begin for intervalID = ' + intervalID)\r
+      console.log('Client: Requesting ICE Candidates from server')\r
+      const request = new XMLHttpRequest()\r
+      request.open('GET', url, true)\r
+      request.setRequestHeader('Content-Type', 'application/json' )\r
+      request.setRequestHeader('X-Strapp-Type', 'ice-candidate-request')\r
+      request.setRequestHeader('X-client-pubkey', pubKey)\r
+      request.onreadystatechange = () => {\r
+        if (request.status === 200) {\r
+          if(request.readyState === 4) {\r
+            console.log('Client: Recieved ICE response from Host')\r
+            let response = JSON.parse(request.response)\r
+            switch(response['iceState']) {\r
+              case "a":\r
+                cpc.addIceCandidate(new RTCIceCandidate(response.ice))\r
+                break\r
+              case "g": /* Gathering so let interval keep polling */\r
+                break\r
+              case "c": /* host iceState == Complete, stop bugging it */\r
+                clearInterval(intervalID)\r
+                clearTimeout()\r
+                break\r
+              default:\r
+                console.log('Unhandled iceState in pollServerForICECandidate()' + response['iceState'])\r
+                break\r
+            }\r
+          }\r
+        }\r
+        else {\r
+          console.log('server unhandled response of status ' + request.status)\r
+          clearInterval(intervalID)\r
+        }\r
+      }\r
+      request.send()\r
+    }\r
+    else {\r
+      clearTimeout()\r
+      clearInterval(intervalID)\r
+    }\r
+  }, 5000)\r
+}\r
+\r
+/* Create and send offer -> Send ICE Candidates -> Poll for ICE Candidates */\r
+getPublicKey().then((cpk) => {\r
+  console.log('Client: Create and send offer')\r
+  const cpc = new RTCPeerConnection(conf)\r
+\r
+  cpc.oniceconnectionstatechange = () => {\r
+    console.log('iceConnectionState = ' + cpc.iceConnectionState)\r
+  }\r
+\r
+  cpc.onnegotiationneeded = () => {\r
+    console.log('negotiation needed!')\r
+    cpc.createOffer().then((offer) => {\r
+      return cpc.setLocalDescription(offer)\r
+    })\r
+    .then(() => {\r
+      console.log('Client: Sending offer to host')\r
+      let offer = {\r
+        cmd: '> sdp pubKey',\r
+        sdp: cpc.localDescription,\r
+        pubKey: cpk.n\r
+      }\r
+      return pollServer(window.location, offer, pollServerForAnswer)\r
+    }).then((serverResponse) => {\r
+      const answer = JSON.parse(serverResponse)\r
+      console.log('Client: Polling for ICE candidates')\r
+      pollServerForICECandidate(cpc, window.location, cpk.n)\r
+      cpc.setRemoteDescription(answer.sdp)\r
+      cpc.onicecandidate = (event) => {\r
+        if (event.candidate) {\r
+          console.log('Client: Sending ice candidate to host')\r
+          postServer(window.location, JSON.stringify({\r
+            cmd: '> ice pubkey',\r
+            ice: event.candidate,\r
+            pubKey: cpk.n\r
+          }))\r
+        }\r
+        else {\r
+          console.log('Client: No more Ice Candidates to send')\r
+        }\r
+      }\r
+\r
+\r
+    }).catch( (err) => {\r
+      console.log('error in sdp handshake: ' + err)\r
+    })\r
+  }\r
+  /* Start data channel */\r
+  dataChannel = cpc.createDataChannel("sendChannel");\r
+  cpc.ontrack = (event) => {\r
+    console.log(`track event is ${event}`)\r
+    let remoteRTPSenders = cpc.getSenders()\r
+    let remoteRTPReceivers = cpc.getReceivers()\r
+    console.log(remoteRTPReceivers)\r
+    /* Add each remoteRTPSenders.track to own stream */\r
+    let video = document.querySelector('video')\r
+    video.autoplay = true\r
+    console.log(video)\r
+    hostScreen = new MediaStream([remoteRTPReceivers[0].track])\r
+    if(!video.srcObject) {\r
+      video.srcObject = hostScreen\r
+    }\r
+    console.log(hostScreen.getVideoTracks())\r
+    console.log(video.srcObject)\r
+    video.play()\r
+    video.onloadedmetadata = () => {\r
+      video.play()\r
+    }\r
+  }\r
+  dataChannel.onmessage = (msg) => {\r
+    /* Get mediaStream from host and add it to the video */\r
+    let hostMessage = JSON.parse(msg.data)\r
+    cpc.setRemoteDescription(hostMessage.sdp).then(() => {\r
+      cpc.createAnswer().then((answer) => {\r
+        return cpc.setLocalDescription(answer)\r
+      }).then(() => {\r
+        dataChannel.send(JSON.stringify({\r
+          "cmd": "> screen dataChannel",\r
+          "sdp": cpc.localDescription\r
+        }))\r
+      })\r
+    })\r
+\r
+\r
+  }\r
+  dataChannel.onopen = () => {\r
+    document.body.innerHTML = (`<div><button> Connection with host established! </button></div> <video controls></video>`)\r
+  }\r
+\r
+})\r
+document.addEventListener('DOMContentLoaded', () => {\r
+\r
+    document.body.innerHTML = `<button> Setting up connection with host </button>`\r
+\r
+});\r
diff --git a/host.js b/host.js
index 81aabc6..9814793 100644 (file)
--- a/host.js
+++ b/host.js
-document.title = "Strapp.io Host"
-
-
-const conf = {"iceServers": [{ "urls": "stun:stun.1.google.com:19302" }] }
-const clients = new Map([])
-const iceCandidates = []
-let dataChannel
-
-
-/* TODO: duplicate in both client.js and host.jhs */
-function getPublicKey() {
-  return new Promise( (resolve, reject) => {
-    /* Check local storage for public key */
-    if (!window.localStorage.getItem('public-key')) {
-      console.log('public key is undefined')
-      /* If doesn't exist, generate public and private key pair, store in
-      local storage */
-      crypto.subtle.generateKey(
-        { name:'RSA-OAEP',
-        modulusLength: 2048,
-        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
-        hash: {name: "SHA-256"}
-      },
-      true,
-      ['encrypt', 'decrypt']
-    ).then((keyPair) => {
-      /* TODO: Do we need to store the private key as well? */
-      crypto.subtle.exportKey('jwk', keyPair.publicKey)
-      .then((exportedKey) => {
-        window.localStorage.setItem('publicKey', exportedKey)
-        console.log('public key is' + window.localStorage.getItem('publicKey'))
-        resolve(exportedKey)
-      })
-
-    })
-  }
-  else {
-    resolve(window.localStorage.getItem('publicKey'))
-    }
-  })
-}
-
-function handleNewClientConnection(offer) {
-  /* New Client Connection*/
-  hpc = new RTCPeerConnection(conf)
-  //console.log(offer)
-  clients.set(offer.pubKey, hpc)
-  hpc.setRemoteDescription(offer.sdp)
-  .then(() => {
-    hpc.createAnswer().then((answer) => {
-      return hpc.setLocalDescription(answer)
-    })
-    .then(() => {
-      getPublicKey().then((hpk) => {
-        hpc.onicecandidate = (event) => {
-          if (event.candidate) {
-            console.log('Host: Allocating ice candidate for client')
-            iceCandidates.push(JSON.stringify({
-              cmd: "< ice pubKey",
-              ice: event.candidate,
-              hostPubKey: hpk.n, /* TODO: do we need to send this? */
-              clientPubKey: offer.pubKey,
-              iceState: "a"
-            }))
-          }
-          else {
-            console.log('Host: Finished sending ICE candidates')
-            console.log(hpc)
-          }
-        }
-        console.log('Host: Sending answer to Client')
-        wsock.send(JSON.stringify({
-          cmd: '< sdp pubKey',
-          sdp: hpc.localDescription,
-          hostPubKey: hpk.n,
-          clientPubKey: offer.pubKey
-        }))
-        hpc.ondatachannel = (evt) => {
-          dataChannel = evt.channel
-          console.log(evt)
-          dataChannel.onmessage = (msg) => {
-            console.log(msg.data)
-          }
-          dataChannel.onopen = () => {
-            dataChannel.send(`Hi ${offer.pubKey} -host`)
-          }
-        }
-        hpc.oniceconnectionstatechange = () => {
-          console.log('iceConnectionState = ' + hpc.iceConnectionState)
-        }
-      })
-    }).catch((err) => {
-      console.log(`error in host answer ${err}`)
-    })
-  })
-
-}
-
-function handleNewIceSubmission(msg) {
-  console.log('Host: Adding new ice candidate')
-  const hpc = clients.get(msg.pubKey)
-  let candidate = new RTCIceCandidate(msg.ice)
-  hpc.addIceCandidate(candidate)
-}
-
-function handleIceRequest(msg) {
-  console.log('Host: Handling ice candidate request')
-  console.log(iceCandidates)
-  const hpc = clients.get(msg.pubKey)
-  const iceCandidate = iceCandidates.pop()
-  if (iceCandidate !== undefined) {
-    wsock.send(iceCandidate)
-  } else {
-    if (hpc.iceGatheringState.localeCompare('gathering') === 0) {
-      wsock.send(`{"cmd" : "< ice pubKey", "clientPubKey":"${msg.pubKey}", "iceState": "g"}`)
-    }
-    else if (hpc.iceGatheringState.localeCompare('complete') === 0) {
-      wsock.send(`{"cmd" : "< ice pubKey", "clientPubKey":"${msg.pubKey}", "iceState": "c"}`)
-    }
-
-  }
-
-}
-if ("WebSocket" in window) {
-  document.addEventListener('DOMContentLoaded', (event) => {
-    wsock = new WebSocket(`${_strapp_protocol}://${window.location.hostname}:${_strapp_port}`)
-    wsock.onopen = () => {
-      console.log(`Strapped to ${_strapp_protocol}://${window.location.hostname}:${_strapp_port}`)
-    }
-
-    wsock.onmessage = (serverMsg) => {
-      /* msg is either offer or ice candidate or ice candidate request*/
-
-      /* What if data null? */
-      let msg = JSON.parse(serverMsg.data)
-
-      const clientID = msg.pubKey
-
-      /* TODO: redo this trash */
-      if (clients.has(clientID)) {
-        if (msg.ice) {
-          handleNewIceSubmission(msg)
-        } else if (msg.sdp) {
-          //handleRepeatedOffer
-        } else {
-          handleIceRequest(msg)
-        }
-      }
-      else {
-        if (msg.ice) {
-          console.log('Host: Client that doesnt exist is sending ice submissions')
-        } else if (msg.sdp) {
-          handleNewClientConnection(msg)
-        } else {
-          console.log('Host: Client that doesnt exist is sending ice requests')
-        }
-      }
-    }
-    document.body.innerHTML = '<div>Choose options for client</div> <video autoplay></video>'
-
-    navigator.mediaDevices.getUserMedia({ video : { mediaSource: "screen", // whole screen sharing
-        // mediaSource: "window", // choose a window to share
-        // mediaSource: "application", // choose a window to share
-        width: {max: '1920'},
-        height: {max: '1080'},
-        frameRate: {max: '10'}} })
-    .then(function(mediaStream) {
-      let video = document.querySelector('video')
-      video.srcObject = mediaStream
-      video.onloadedmetadata = function(e) {
-        video.play()
-      }
-    })
-    .catch(function(err) { console.log(err); }); // always check for errors at the end.
-  })
-}
-else {
-  document.addEventListener('DOMContentLoaded', () => {
-    document.body.innerHTML = 'Websockets not supported in your browser'
-  })
-}
+document.title = "Strapp.io Host"\r
+\r
+\r
+const conf = {"iceServers": [{ "urls": "stun:stun.1.google.com:19302" }] }\r
+const clients = new Map([])\r
+const iceCandidates = []\r
+let dataChannel\r
+let screenStream /* TODO: Remove if can access localStreams */\r
+let tracks = []\r
+\r
+\r
+/* TODO: duplicate in both client.js and host.jhs */\r
+function getPublicKey() {\r
+  return new Promise( (resolve, reject) => {\r
+    /* Check local storage for public key */\r
+    if (!window.localStorage.getItem('public-key')) {\r
+      console.log('public key is undefined')\r
+      /* If doesn't exist, generate public and private key pair, store in\r
+      local storage */\r
+      crypto.subtle.generateKey(\r
+        { name:'RSA-OAEP',\r
+        modulusLength: 2048,\r
+        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),\r
+        hash: {name: "SHA-256"}\r
+      },\r
+      true,\r
+      ['encrypt', 'decrypt']\r
+    ).then((keyPair) => {\r
+      /* TODO: Do we need to store the private key as well? */\r
+      crypto.subtle.exportKey('jwk', keyPair.publicKey)\r
+      .then((exportedKey) => {\r
+        window.localStorage.setItem('publicKey', exportedKey)\r
+        console.log('public key is' + window.localStorage.getItem('publicKey'))\r
+        resolve(exportedKey)\r
+      })\r
+\r
+    })\r
+  }\r
+  else {\r
+    resolve(window.localStorage.getItem('publicKey'))\r
+    }\r
+  })\r
+}\r
+\r
+function handleNewClientConnection(offer) {\r
+  /* New Client Connection*/\r
+  hpc = new RTCPeerConnection(conf)\r
+  //console.log(offer)\r
+  clients.set(offer.pubKey, hpc)\r
+  hpc.setRemoteDescription(offer.sdp)\r
+  .then(() => {\r
+    hpc.createAnswer().then((answer) => {\r
+      return hpc.setLocalDescription(answer)\r
+    })\r
+    .then(() => {\r
+      getPublicKey().then((hpk) => {\r
+        hpc['hpk'] = hpk\r
+        hpc.onicecandidate = (event) => {\r
+          if (event.candidate) {\r
+            console.log('Host: Allocating ice candidate for client')\r
+            iceCandidates.push(JSON.stringify({\r
+              cmd: "< ice pubKey",\r
+              ice: event.candidate,\r
+              hostPubKey: hpk.n, /* TODO: do we need to send this? */\r
+              clientPubKey: offer.pubKey,\r
+              iceState: "a"\r
+            }))\r
+          }\r
+          else {\r
+            console.log('Host: Finished sending ICE candidates')\r
+          }\r
+        }\r
+        console.log('Host: Sending answer to Client')\r
+        wsock.send(JSON.stringify({\r
+          cmd: '< sdp pubKey',\r
+          sdp: hpc.localDescription,\r
+          hostPubKey: hpk.n,\r
+          clientPubKey: offer.pubKey\r
+        }))\r
+\r
+\r
+      })\r
+    }).catch((err) => {\r
+      console.log(`error in host answer ${err}`)\r
+    })\r
+  })\r
+  hpc.oniceconnectionstatechange = () => {\r
+    console.log('iceConnectionState = ' + hpc.iceConnectionState)\r
+  }\r
+  hpc.ondatachannel = (evt) => {\r
+    dataChannel = evt.channel\r
+    dataChannel.onmessage = (msg) => {\r
+      let clientMessage = JSON.parse(msg.data)\r
+      console.log(clientMessage)\r
+      hpc.setRemoteDescription(clientMessage.sdp).then(() => {\r
+        console.log('should be streaming now')\r
+      })\r
+    }\r
+    dataChannel.onopen = () => {\r
+      screenStream.getTracks().forEach( (track) => {\r
+        hpc.addTrack(track, screenStream)\r
+      })\r
+      console.log(hpc.getSenders())\r
+      /* Create offer */\r
+      hpc.createOffer().then((offer) => {\r
+        return hpc.setLocalDescription(offer)\r
+      }).then( () => {\r
+        dataChannel.send(JSON.stringify({\r
+          "cmd": "< screen dataChannel",\r
+          "sdp": hpc.localDescription,\r
+          "pubKey": hpc['hpk'].n\r
+        }))\r
+      })\r
+    }\r
+  }\r
+  hpc.onnegotiationneeded = () => {\r
+    console.log('negotiation needed')\r
+  }\r
+\r
+}\r
+\r
+function handleNewIceSubmission(msg) {\r
+  console.log('Host: Adding new ice candidate')\r
+  const hpc = clients.get(msg.pubKey)\r
+  let candidate = new RTCIceCandidate(msg.ice)\r
+  hpc.addIceCandidate(candidate)\r
+}\r
+\r
+function handleIceRequest(msg) {\r
+  console.log('Host: Handling ice candidate request')\r
+  console.log(iceCandidates)\r
+  const hpc = clients.get(msg.pubKey)\r
+  const iceCandidate = iceCandidates.pop()\r
+  if (iceCandidate !== undefined) {\r
+    wsock.send(iceCandidate)\r
+  } else {\r
+    if (hpc.iceGatheringState.localeCompare('gathering') === 0) {\r
+      wsock.send(`{"cmd" : "< ice pubKey", "clientPubKey":"${msg.pubKey}", "iceState": "g"}`)\r
+    }\r
+    else if (hpc.iceGatheringState.localeCompare('complete') === 0) {\r
+      wsock.send(`{"cmd" : "< ice pubKey", "clientPubKey":"${msg.pubKey}", "iceState": "c"}`)\r
+    }\r
+\r
+  }\r
+\r
+}\r
+if ("WebSocket" in window) {\r
+  document.addEventListener('DOMContentLoaded', (event) => {\r
+    document.body.innerHTML = '<div>Choose options for client</div> <video autoplay></video>'\r
+    navigator.mediaDevices.getUserMedia({\r
+        video : { mediaSource: "screen",\r
+        width: {max: '1920'},\r
+        height: {max: '1080'},\r
+        frameRate: {max: '10'}} })\r
+    .then(function(mediaStream) {\r
+      let video = document.querySelector('video')\r
+      screenStream = mediaStream\r
+      console.log(mediaStream)\r
+      video.srcObject = mediaStream\r
+      console.log('Grabbed media')\r
+      video.onloadedmetadata = function(e) {\r
+        console.log(e)\r
+        video.play()\r
+      }\r
+    })\r
+    .catch(function(err) {\r
+      document.body.innerHTML = 'Help me help you. Reload the page and allow screen sharing!'\r
+      console.log(err);\r
+    }); // always check for errors at the end.\r
+\r
+    wsock = new WebSocket(`${_strapp_protocol}://${window.location.hostname}:${_strapp_port}`)\r
+    wsock.onopen = () => {\r
+      console.log(`Strapped to ${_strapp_protocol}://${window.location.hostname}:${_strapp_port}`)\r
+    }\r
+\r
+    wsock.onmessage = (serverMsg) => {\r
+      /* msg is either offer or ice candidate or ice candidate request*/\r
+\r
+      /* What if data null? */\r
+      let msg = JSON.parse(serverMsg.data)\r
+\r
+      const clientID = msg.pubKey\r
+\r
+      /* TODO: redo this trash */\r
+      if (clients.has(clientID)) {\r
+        if (msg.ice) {\r
+          handleNewIceSubmission(msg)\r
+        } else if (msg.sdp) {\r
+          //handleRepeatedOffer\r
+        } else {\r
+          handleIceRequest(msg)\r
+        }\r
+      }\r
+      else {\r
+        if (msg.ice) {\r
+          console.log('Host: Client that doesnt exist is sending ice submissions')\r
+        } else if (msg.sdp) {\r
+          handleNewClientConnection(msg)\r
+        } else {\r
+          console.log('Host: Client that doesnt exist is sending ice requests')\r
+        }\r
+      }\r
+    }\r
+\r
+\r
+\r
+  })\r
+}\r
+else {\r
+  document.addEventListener('DOMContentLoaded', () => {\r
+    document.body.innerHTML = 'Websockets not supported in your browser'\r
+  })\r
+}\r
diff --git a/main.js b/main.js
index 321af88..136aee1 100644 (file)
--- a/main.js
+++ b/main.js
-/**
-* @file      Node entry and main driver
-* @author    Jordan Lavatai, Ken Grimes
-* @version   0.0.1
-* @license   AGPL-3.0
-* @copyright loljk 2017
-* @summary   HTTP(S) Router that uses the first directory in the requested URL
-*            as the route name
-*/
-const fs = require('fs')
-const ws = require('ws')
-const path = require('path')
-const http = require('http')
-const https = require('https')
-const getport = require('get-port')
-const mime = require('mime')
-const opts = require('./opts.js')
-
-const router = {
-  skelPage:   fs.readFileSync('./skel.html', { encoding: 'utf8' }).split('<!--STRAPP_SRC-->'),
-  clientJS:   fs.readFileSync(opts['client-js']),
-  hostJS:     fs.readFileSync(opts['host-js']),
-  routes:     {},
-  httpsOpt:   undefined,
-  httpd:      undefined,
-  wsProtocol: opts['no-tls'] ? 'ws' : 'wss',
-  respond:    (request,response) => {
-    console.log('server handling request')
-    const serveFile = (fPath) => {
-      fs.readFile(fPath, { encoding: 'utf8' }, (err, data) => {
-        if (err || data == undefined) {
-          response.writeHead(404)
-          response.end()
-        }
-        else {
-          response.writeHead(200, { 'Content-Type': mime.lookup(fPath) })
-          response.write(data)
-          response.end()
-        }
-      })
-    }
-    const htArgv = request.url.slice(1).split("?")
-    let routePath = htArgv[0].split('/')
-    let routeName = routePath[0]
-
-
-    if (routeName === '' || routeName === 'index.html')
-    serveFile(opts['index'])
-    else if (routeName in opts['bindings']) {
-      let localPath = path.normalize(opts['bindings'][routeName].concat(path.sep + routePath.slice(1).join(path.sep)))
-      if (localPath.includes(opts['bindings'][routeName])) {
-        fs.readdir(localPath, (err, files) => {
-          if (err)
-          serveFile(localPath)
-          else
-          serveFile(`${localPath}/index.html`)
-        })
-      }
-      else {
-        console.log(`SEC: ${localPath} references files not in route`)
-      }
-    }
-    /* TODO: Handle reconnecting host */
-    else if (routeName in router.routes) {
-      const route = router.routes[routeName]
-      const clients = route['clients']
-      const headerData = request.headers['x-strapp-type']
-
-
-
-
-      /* Client is INIT GET */
-      if (headerData === undefined) {
-        console.log('client init GET')
-        response.writeHead(200, { 'Content-Type': 'text/html' })
-        response.write(`${router.skelPage[0]}${router.clientJS}${router.skelPage[1]}`)
-        response.end()
-        //TODO: if route.socket == undefined: have server delay this send until host connects
-        //      (this happens when a client connects to an active route with no currently-online host)
-      }
-      else if (headerData.localeCompare('ice-candidate-request') === 0) {
-        console.log('Server: received ice-candidate-request from Client')
-        let pubKey = request.headers['x-client-pubkey']
-        clients.set(pubKey, response)
-        pubKey = '{ "pubKey": "'  + pubKey + '" }'
-        route.socket.send(pubKey)
-      }
-      else if (headerData.localeCompare('ice-candidate-submission') === 0) {
-        console.log('Server: recieved ice-candidate-submission from Client')
-        let data = []
-        request.on('data', (chunk) => {
-          data.push(chunk)
-        }).on('end', () => {
-          console.log('Sending ice-candidate-submission to Host')
-          data = Buffer.concat(data).toString();
-          clients.set(JSON.parse(data)['pubKey'], response)
-          route.socket.send(data)
-        })
-      }
-      else if (headerData.localeCompare('client-sdp-offer') === 0){ /* Client sent offer, waiting for answer */
-        console.log('Server: Sending client offer to host')
-        clients.set(JSON.parse(request.headers['x-client-offer'])['pubKey'], response)
-        route.socket.send(request.headers['x-client-offer'])
-      } else {
-        console.log('Unhandled stuff')
-        console.log(request.headers)
-      }
-
-    }
-    else {
-      router.routes[routeName] = true
-      const newRoute = {}
-      newRoute.clients = new Map([])
-      newRoute.host = request.headers['x-forwarded-for'] || request.connection.remoteAddress
-      getport().then( (port) => {
-        newRoute.port = port
-        if (opts['no-tls'])
-        newRoute.httpd = http.createServer()
-        else
-        newRoute.httpd = https.createServer(router.httpsOpts)
-        newRoute.httpd.listen(newRoute.port)
-        newRoute.wsd = new ws.Server( { server: newRoute.httpd } )
-        newRoute.wsd.on('connection', (sock) => {
-          console.log(`${routeName} server has been established`)
-          newRoute.socket = sock
-
-          /* Handle all messages from host */
-          sock.on('message', (hostMessage) => {
-            hostMessage = JSON.parse(hostMessage)
-            response = newRoute.clients.get(hostMessage['clientPubKey'])
-
-            /* If the host response is a answer */
-            if (hostMessage['cmd'].localeCompare('< sdp pubKey') === 0) {
-              console.log('Server: Sending host answer to client')
-              response.writeHead(200, { 'Content-Type': 'application/json' })
-              response.write(JSON.stringify(hostMessage))
-              response.end()
-            }
-            else if (hostMessage['cmd'].localeCompare('< ice pubKey') === 0){
-              /* if the host response is an ice candidate */
-              console.log('Server: Handling host ICE message')
-              let iceState = hostMessage['iceState']              
-              /* If there are any ice candidates, send them back */
-              switch(iceState) {
-                case "a":
-                  response.writeHead('200', {'x-strapp-type': 'ice-candidate-available'})
-                  response.write(JSON.stringify(hostMessage))
-                  response.end()
-                  break
-                case "g":
-                  console.log('Server: Host is still gathering candidates, keep trying')
-                  response.writeHead('200', {'x-strapp-type': 'ice-state-gathering'})
-                  response.write(JSON.stringify(hostMessage))
-                  response.end()
-                break
-                case "c":
-                  console.log('Server: Host has completed gathering candidates')
-                  response.writeHead('200', {'x-strapp-type': 'ice-state-complete'})
-                  response.write(JSON.stringify(hostMessage))
-                  response.end()
-                  break
-                default:
-                  console.log('unhandled iceState from host')
-                  break
-                }
-            }
-
-          })
-        })
-
-        console.log(`Listening for websocket ${newRoute.host} on port ${newRoute.port}`)
-        router.routes[routeName] = newRoute
-      }).then(() => {
-        response.writeHead(200, { 'Content-Type': 'text/html' })
-        response.write(`${router.skelPage[0]}` +
-          `\tconst _strapp_port = ${newRoute.port}\n` +
-          `\tconst _strapp_protocol = '${router.wsProtocol}'\n` +
-          `${router.hostJS}\n${router.skelPage[1]}`)
-          response.end()
-        })
-      }
-
-
-    }
-  }
-
-  /**
-  * @summary Boot up the router.  With TLS, we must wait for file reads to sync.
-  */
-  if (!opts['no-tls']) {
-    console.log('tls')
-    let filesRead = 0
-    let key = undefined
-    let cert = undefined
-    const syncRead = () => {
-      if (++filesRead == 2) {
-        if (key == undefined)
-        console.log(`ERR: Key ${opts['ca-key']} inaccessible, tls will fail`)
-        if(cert == undefined)
-        console.log(`ERR: Cert ${opts['ca-cert']} inaccessible, tls will fail`)
-        else if (key != undefined) {
-          router.httpsOpts = { cert: cert, key: key}
-          router.httpd = https.createServer(router.httpsOpts, router.respond)
-          .listen(opts['port'])
-        }
-      }
-    }
-    fs.readFile(opts['ca-key'], { encoding: 'utf8' }, (err, data) => {
-      if (!err) key = data
-      syncRead()
-    })
-    fs.readFile(opts['ca-cert'], { encoding: 'utf8' }, (err, data) => {
-      if (!err) cert = data
-      syncRead()
-    })
-  }
-  else
-  router.httpd = http.createServer(router.respond).listen(opts['port'])
-
-  //TODO: if ("electron" in process.versions) open a local renderwindow, and route to it
+/**\r
+* @file      Node entry and main driver\r
+* @author    Jordan Lavatai, Ken Grimes\r
+* @version   0.0.1\r
+* @license   AGPL-3.0\r
+* @copyright loljk 2017\r
+* @summ ary   HTTP(S) Router that uses the first directory in the requested URL\r
+*            as the route name\r
+*/\r
+const fs = require('fs')\r
+const ws = require('ws')\r
+const path = require('path')\r
+const http = require('http')\r
+const https = require('https')\r
+const getport = require('get-port')\r
+const mime = require('mime')\r
+const opts = require('./opts.js')\r
+\r
+const router = {\r
+  skelPage:   fs.readFileSync('./skel.html', { encoding: 'utf8' }).split('<!--STRAPP_SRC-->'),\r
+  clientJS:   fs.readFileSync(opts['client-js']),\r
+  hostJS:     fs.readFileSync(opts['host-js']),\r
+  routes:     {},\r
+  httpsOpt:   undefined,\r
+  httpd:      undefined,\r
+  wsProtocol: opts['no-tls'] ? 'ws' : 'wss',\r
+  respond:    (request,response) => {\r
+    console.log('server handling request')\r
+    const serveFile = (fPath) => {\r
+      fs.readFile(fPath, { encoding: 'utf8' }, (err, data) => {\r
+        if (err || data == undefined) {\r
+          response.writeHead(404)\r
+          response.end()\r
+        }\r
+        else {\r
+          response.writeHead(200, { 'Content-Type': mime.lookup(fPath) })\r
+          response.write(data)\r
+          response.end()\r
+        }\r
+      })\r
+    }\r
+    const htArgv = request.url.slice(1).split("?")\r
+    let routePath = htArgv[0].split('/')\r
+    let routeName = routePath[0]\r
+\r
+\r
+    if (routeName === '' || routeName === 'index.html')\r
+    serveFile(opts['index'])\r
+    else if (routeName in opts['bindings']) {\r
+      let localPath = path.normalize(opts['bindings'][routeName].concat(path.sep + routePath.slice(1).join(path.sep)))\r
+      if (localPath.includes(opts['bindings'][routeName])) {\r
+        fs.readdir(localPath, (err, files) => {\r
+          if (err)\r
+          serveFile(localPath)\r
+          else\r
+          serveFile(`${localPath}/index.html`)\r
+        })\r
+      }\r
+      else {\r
+        console.log(`SEC: ${localPath} references files not in route`)\r
+      }\r
+    }\r
+    /* TODO: Handle reconnecting host */\r
+    else if (routeName in router.routes) {\r
+      const route = router.routes[routeName]\r
+      const clients = route['clients']\r
+      const headerData = request.headers['x-strapp-type']\r
+\r
+\r
+\r
+\r
+      /* Client is INIT GET */\r
+      if (headerData === undefined) {\r
+        console.log('client init GET')\r
+        response.writeHead(200, { 'Content-Type': 'text/html' })\r
+        response.write(`${router.skelPage[0]}${router.clientJS}${router.skelPage[1]}`)\r
+        response.end()\r
+        //TODO: if route.socket == undefined: have server delay this send until host connects\r
+        //      (this happens when a client connects to an active route with no currently-online host)\r
+      }\r
+      else if (headerData.localeCompare('ice-candidate-request') === 0) {\r
+        console.log('Server: received ice-candidate-request from Client')\r
+        let pubKey = request.headers['x-client-pubkey']\r
+        clients.set(pubKey, response)\r
+        pubKey = '{ "pubKey": "'  + pubKey + '" }'\r
+        route.socket.send(pubKey)\r
+      }\r
+      else if (headerData.localeCompare('ice-candidate-submission') === 0) {\r
+        console.log('Server: recieved ice-candidate-submission from Client')\r
+        let data = []\r
+        request.on('data', (chunk) => {\r
+          data.push(chunk)\r
+        }).on('end', () => {\r
+          console.log('Sending ice-candidate-submission to Host')\r
+          data = Buffer.concat(data).toString();\r
+          clients.set(JSON.parse(data)['pubKey'], response)\r
+          route.socket.send(data)\r
+        })\r
+      }\r
+      else if (headerData.localeCompare('client-sdp-offer') === 0){ /* Client sent offer, waiting for answer */\r
+        console.log('Server: Sending client offer to host')\r
+        clients.set(JSON.parse(request.headers['x-client-offer'])['pubKey'], response)\r
+        route.socket.send(request.headers['x-client-offer'])\r
+      } else {\r
+        console.log('Unhandled stuff')\r
+        console.log(request.headers)\r
+      }\r
+\r
+    }\r
+    else {\r
+      router.routes[routeName] = true\r
+      const newRoute = {}\r
+      newRoute.clients = new Map([])\r
+      newRoute.host = request.headers['x-forwarded-for'] || request.connection.remoteAddress\r
+      getport().then( (port) => {\r
+        newRoute.port = port\r
+        if (opts['no-tls'])\r
+        newRoute.httpd = http.createServer()\r
+        else\r
+        newRoute.httpd = https.createServer(router.httpsOpts)\r
+        newRoute.httpd.listen(newRoute.port)\r
+        newRoute.wsd = new ws.Server( { server: newRoute.httpd } )\r
+        newRoute.wsd.on('connection', (sock) => {\r
+          console.log(`${routeName} server has been established`)\r
+          newRoute.socket = sock\r
+\r
+          /* Handle all messages from host */\r
+          sock.on('message', (hostMessage) => {\r
+            hostMessage = JSON.parse(hostMessage)\r
+            response = newRoute.clients.get(hostMessage['clientPubKey'])\r
+\r
+            /* If the host response is a answer */\r
+            if (hostMessage['cmd'].localeCompare('< sdp pubKey') === 0) {\r
+              console.log('Server: Sending host answer to client')\r
+              response.writeHead(200, { 'Content-Type': 'application/json' })\r
+              response.write(JSON.stringify(hostMessage))\r
+              response.end()\r
+            }\r
+            else if (hostMessage['cmd'].localeCompare('< ice pubKey') === 0){\r
+              /* if the host response is an ice candidate */\r
+              console.log('Server: Handling host ICE message')\r
+              let iceState = hostMessage['iceState']\r
+              /* If there are any ice candidates, send them back */\r
+              switch(iceState) {\r
+                case "a":\r
+                  response.writeHead('200', {'x-strapp-type': 'ice-candidate-available'})\r
+                  response.write(JSON.stringify(hostMessage))\r
+                  response.end()\r
+                  break\r
+                case "g":\r
+                  console.log('Server: Host is still gathering candidates, keep trying')\r
+                  response.writeHead('200', {'x-strapp-type': 'ice-state-gathering'})\r
+                  response.write(JSON.stringify(hostMessage))\r
+                  response.end()\r
+                break\r
+                case "c":\r
+                  console.log('Server: Host has completed gathering candidates')\r
+                  response.writeHead('200', {'x-strapp-type': 'ice-state-complete'})\r
+                  response.write(JSON.stringify(hostMessage))\r
+                  response.end()\r
+                  break\r
+                default:\r
+                  console.log('unhandled iceState from host')\r
+                  break\r
+                }\r
+            }\r
+\r
+          })\r
+        })\r
+\r
+        console.log(`Listening for websocket ${newRoute.host} on port ${newRoute.port}`)\r
+        router.routes[routeName] = newRoute\r
+      }).then(() => {\r
+        response.writeHead(200, { 'Content-Type': 'text/html' })\r
+        response.write(`${router.skelPage[0]}` +\r
+          `\tconst _strapp_port = ${newRoute.port}\n` +\r
+          `\tconst _strapp_protocol = '${router.wsProtocol}'\n` +\r
+          `${router.hostJS}\n${router.skelPage[1]}`)\r
+          response.end()\r
+        })\r
+      }\r
+\r
+\r
+    }\r
+  }\r
+\r
+  /**\r
+  * @summary Boot up the router.  With TLS, we must wait for file reads to sync.\r
+  */\r
+  if (!opts['no-tls']) {\r
+    console.log('tls')\r
+    let filesRead = 0\r
+    let key = undefined\r
+    let cert = undefined\r
+    const syncRead = () => {\r
+      if (++filesRead == 2) {\r
+        if (key == undefined)\r
+        console.log(`ERR: Key ${opts['ca-key']} inaccessible, tls will fail`)\r
+        if(cert == undefined)\r
+        console.log(`ERR: Cert ${opts['ca-cert']} inaccessible, tls will fail`)\r
+        else if (key != undefined) {\r
+          router.httpsOpts = { cert: cert, key: key}\r
+          router.httpd = https.createServer(router.httpsOpts, router.respond)\r
+          .listen(opts['port'])\r
+        }\r
+      }\r
+    }\r
+    fs.readFile(opts['ca-key'], { encoding: 'utf8' }, (err, data) => {\r
+      if (!err) key = data\r
+      syncRead()\r
+    })\r
+    fs.readFile(opts['ca-cert'], { encoding: 'utf8' }, (err, data) => {\r
+      if (!err) cert = data\r
+      syncRead()\r
+    })\r
+  }\r
+  else\r
+  router.httpd = http.createServer(router.respond).listen(opts['port'])\r
+\r
+  //TODO: if ("electron" in process.versions) open a local renderwindow, and route to it\r
diff --git a/package-lock.json b/package-lock.json
new file mode 100644 (file)
index 0000000..7d8e3ad
--- /dev/null
@@ -0,0 +1,677 @@
+{
+  "name": "strapp",
+  "version": "0.0.1",
+  "lockfileVersion": 1,
+  "dependencies": {
+    "@types/node": {
+      "version": "https://registry.npmjs.org/@types/node/-/node-7.0.32.tgz",
+      "integrity": "sha1-av5sZlIKTDFmI6FK7xI5CNAbS7o="
+    },
+    "ajv": {
+      "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
+      "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY="
+    },
+    "ansi-regex": {
+      "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+    },
+    "array-find-index": {
+      "version": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
+    },
+    "asn1": {
+      "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
+      "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
+    },
+    "assert-plus": {
+      "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
+      "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ="
+    },
+    "asynckit": {
+      "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+    },
+    "aws-sign2": {
+      "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
+      "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8="
+    },
+    "aws4": {
+      "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
+      "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
+    },
+    "balanced-match": {
+      "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "bcrypt-pbkdf": {
+      "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
+      "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
+      "optional": true
+    },
+    "boom": {
+      "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
+      "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8="
+    },
+    "brace-expansion": {
+      "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
+      "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI="
+    },
+    "builtin-modules": {
+      "version": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
+    },
+    "camelcase": {
+      "version": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+    },
+    "camelcase-keys": {
+      "version": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc="
+    },
+    "caseless": {
+      "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+    },
+    "co": {
+      "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
+    },
+    "code-point-at": {
+      "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
+    },
+    "combined-stream": {
+      "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
+      "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk="
+    },
+    "concat-map": {
+      "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "concat-stream": {
+      "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
+      "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
+      "dependencies": {
+        "isarray": {
+          "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        },
+        "readable-stream": {
+          "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.2.tgz",
+          "integrity": "sha1-WgTfBeT1f+Pw3Gj90R3FyXx+b00="
+        },
+        "string_decoder": {
+          "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+          "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs="
+        }
+      }
+    },
+    "core-util-is": {
+      "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "cryptiles": {
+      "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
+      "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g="
+    },
+    "currently-unhandled": {
+      "version": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o="
+    },
+    "dashdash": {
+      "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dependencies": {
+        "assert-plus": {
+          "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+          "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+        }
+      }
+    },
+    "debug": {
+      "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
+      "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw="
+    },
+    "decamelize": {
+      "version": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+    },
+    "deep-extend": {
+      "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz",
+      "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8="
+    },
+    "delayed-stream": {
+      "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+    },
+    "ecc-jsbn": {
+      "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
+      "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
+      "optional": true
+    },
+    "electron": {
+      "version": "https://registry.npmjs.org/electron/-/electron-1.6.11.tgz",
+      "integrity": "sha1-vnnA69zv7bW/KBF0CYAPpTus7/o="
+    },
+    "electron-download": {
+      "version": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
+      "integrity": "sha1-LP1U1pZsAZxNSa1l++Zcyc3vaMg="
+    },
+    "error-ex": {
+      "version": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
+      "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw="
+    },
+    "es6-promise": {
+      "version": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.0.tgz",
+      "integrity": "sha1-3aA8qPn4m8WX5omEKSnee6jOvfA="
+    },
+    "extend": {
+      "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
+      "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
+    },
+    "extract-zip": {
+      "version": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.5.tgz",
+      "integrity": "sha1-maBnNbbqIOqbcF13ms/8yHz/BEA=",
+      "dependencies": {
+        "debug": {
+          "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo="
+        },
+        "ms": {
+          "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
+          "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
+        }
+      }
+    },
+    "extsprintf": {
+      "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz",
+      "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA="
+    },
+    "fd-slicer": {
+      "version": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+      "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU="
+    },
+    "find-up": {
+      "version": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8="
+    },
+    "forever-agent": {
+      "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+    },
+    "form-data": {
+      "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
+      "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE="
+    },
+    "fs-extra": {
+      "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz",
+      "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A="
+    },
+    "fs.realpath": {
+      "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "get-port": {
+      "version": "https://registry.npmjs.org/get-port/-/get-port-3.1.0.tgz",
+      "integrity": "sha1-7wGxioTKZIaXD/meVERhQac//T4="
+    },
+    "get-stdin": {
+      "version": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
+    },
+    "getpass": {
+      "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dependencies": {
+        "assert-plus": {
+          "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+          "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+        }
+      }
+    },
+    "glob": {
+      "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+      "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU="
+    },
+    "graceful-fs": {
+      "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
+    },
+    "har-schema": {
+      "version": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz",
+      "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4="
+    },
+    "har-validator": {
+      "version": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
+      "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio="
+    },
+    "hawk": {
+      "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
+      "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ="
+    },
+    "hoek": {
+      "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
+      "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
+    },
+    "home-path": {
+      "version": "https://registry.npmjs.org/home-path/-/home-path-1.0.5.tgz",
+      "integrity": "sha1-eIspgVsS1Tus9XVkhHbm+QQdEz8="
+    },
+    "hosted-git-info": {
+      "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.4.2.tgz",
+      "integrity": "sha1-AHa59GonBQbduq6lZJaJdGBhKmc="
+    },
+    "http-signature": {
+      "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
+      "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8="
+    },
+    "indent-string": {
+      "version": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA="
+    },
+    "inflight": {
+      "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk="
+    },
+    "inherits": {
+      "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "ini": {
+      "version": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz",
+      "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4="
+    },
+    "ip": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+      "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
+    },
+    "is-arrayish": {
+      "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+    },
+    "is-builtin-module": {
+      "version": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74="
+    },
+    "is-finite": {
+      "version": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko="
+    },
+    "is-fullwidth-code-point": {
+      "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs="
+    },
+    "is-typedarray": {
+      "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+    },
+    "is-utf8": {
+      "version": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
+    },
+    "isarray": {
+      "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+    },
+    "isstream": {
+      "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+    },
+    "jsbn": {
+      "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "optional": true
+    },
+    "json-schema": {
+      "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+    },
+    "json-stable-stringify": {
+      "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
+      "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8="
+    },
+    "json-stringify-safe": {
+      "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+    },
+    "jsonfile": {
+      "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+      "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug="
+    },
+    "jsonify": {
+      "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+      "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
+    },
+    "jsprim": {
+      "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz",
+      "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=",
+      "dependencies": {
+        "assert-plus": {
+          "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+          "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+        }
+      }
+    },
+    "klaw": {
+      "version": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
+      "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk="
+    },
+    "load-json-file": {
+      "version": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA="
+    },
+    "loud-rejection": {
+      "version": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8="
+    },
+    "map-obj": {
+      "version": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
+    },
+    "meow": {
+      "version": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs="
+    },
+    "mime": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz",
+      "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA="
+    },
+    "mime-db": {
+      "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz",
+      "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE="
+    },
+    "mime-types": {
+      "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz",
+      "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0="
+    },
+    "minimatch": {
+      "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM="
+    },
+    "minimist": {
+      "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+    },
+    "mkdirp": {
+      "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
+      "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
+      "dependencies": {
+        "minimist": {
+          "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+        }
+      }
+    },
+    "ms": {
+      "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "normalize-package-data": {
+      "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.8.tgz",
+      "integrity": "sha1-2Bntoqne29H/pWPqQHHZNngilbs="
+    },
+    "nugget": {
+      "version": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz",
+      "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA="
+    },
+    "number-is-nan": {
+      "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+    },
+    "oauth-sign": {
+      "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+      "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
+    },
+    "object-assign": {
+      "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "object-keys": {
+      "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz",
+      "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY="
+    },
+    "once": {
+      "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E="
+    },
+    "parse-json": {
+      "version": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck="
+    },
+    "path-exists": {
+      "version": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+      "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s="
+    },
+    "path-is-absolute": {
+      "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-type": {
+      "version": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+      "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE="
+    },
+    "pend": {
+      "version": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
+    },
+    "performance-now": {
+      "version": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
+      "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU="
+    },
+    "pify": {
+      "version": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+    },
+    "pinkie": {
+      "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+    },
+    "pinkie-promise": {
+      "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o="
+    },
+    "pretty-bytes": {
+      "version": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz",
+      "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ="
+    },
+    "process-nextick-args": {
+      "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+      "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
+    },
+    "progress-stream": {
+      "version": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz",
+      "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c="
+    },
+    "punycode": {
+      "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+    },
+    "qs": {
+      "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
+      "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM="
+    },
+    "rc": {
+      "version": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz",
+      "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU="
+    },
+    "read-pkg": {
+      "version": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg="
+    },
+    "read-pkg-up": {
+      "version": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI="
+    },
+    "readable-stream": {
+      "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+      "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk="
+    },
+    "redent": {
+      "version": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94="
+    },
+    "repeating": {
+      "version": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo="
+    },
+    "request": {
+      "version": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
+      "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA="
+    },
+    "rimraf": {
+      "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz",
+      "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0="
+    },
+    "safe-buffer": {
+      "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+      "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM="
+    },
+    "semver": {
+      "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+      "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
+    },
+    "signal-exit": {
+      "version": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
+    },
+    "single-line-log": {
+      "version": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz",
+      "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q="
+    },
+    "sntp": {
+      "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
+      "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg="
+    },
+    "spdx-correct": {
+      "version": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
+      "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A="
+    },
+    "spdx-expression-parse": {
+      "version": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz",
+      "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw="
+    },
+    "spdx-license-ids": {
+      "version": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
+      "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc="
+    },
+    "speedometer": {
+      "version": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz",
+      "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0="
+    },
+    "sshpk": {
+      "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
+      "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
+      "dependencies": {
+        "assert-plus": {
+          "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+          "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+        }
+      }
+    },
+    "string_decoder": {
+      "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+      "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+    },
+    "string-width": {
+      "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M="
+    },
+    "stringstream": {
+      "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
+      "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
+    },
+    "strip-ansi": {
+      "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8="
+    },
+    "strip-bom": {
+      "version": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4="
+    },
+    "strip-indent": {
+      "version": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI="
+    },
+    "strip-json-comments": {
+      "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+    },
+    "sumchecker": {
+      "version": "https://registry.npmjs.org/sumchecker/-/sumchecker-1.3.1.tgz",
+      "integrity": "sha1-ebs7RFbdBPGOvbwNcDodHa7FEF0="
+    },
+    "throttleit": {
+      "version": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz",
+      "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8="
+    },
+    "through2": {
+      "version": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz",
+      "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8="
+    },
+    "tough-cookie": {
+      "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
+      "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo="
+    },
+    "trim-newlines": {
+      "version": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
+    },
+    "tunnel-agent": {
+      "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0="
+    },
+    "tweetnacl": {
+      "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "optional": true
+    },
+    "typedarray": {
+      "version": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
+    "ultron": {
+      "version": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz",
+      "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ="
+    },
+    "util-deprecate": {
+      "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+    },
+    "uuid": {
+      "version": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
+      "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ="
+    },
+    "validate-npm-package-license": {
+      "version": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
+      "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w="
+    },
+    "verror": {
+      "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz",
+      "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw="
+    },
+    "wrappy": {
+      "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "ws": {
+      "version": "https://registry.npmjs.org/ws/-/ws-3.0.0.tgz",
+      "integrity": "sha1-mN2wAFbIOQy3Ued4h4hJf5kQO2w=",
+      "dependencies": {
+        "safe-buffer": {
+          "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz",
+          "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c="
+        }
+      }
+    },
+    "xtend": {
+      "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
+      "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os="
+    },
+    "yauzl": {
+      "version": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+      "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU="
+    }
+  }
+}
diff --git a/router.js b/router.js
new file mode 100644 (file)
index 0000000..2f0441a
--- /dev/null
+++ b/router.js
@@ -0,0 +1,22 @@
+/**
+ * @file      HTTP(S) Router that treats the first directory in a URL's path as
+ *            a route to a host.
+ * @author    Ken Grimes
+ * @version   0.0.1
+ * @license   AGPL-3.0
+ * @copyright jk software 2017
+ */
+const fs = require('fs')
+
+module.exports = (opts) => {
+  const startHttpd = (listener) => {
+    if (opts['no-tls'])
+      return require('http').createServer(listener)
+    if (fs.existsSync(opts['ca-cert']))
+      
+  }
+  return {
+    httpd: startHttpd(),
+    wsd:   startWsd()
+  }
+}
diff --git a/usage b/usage
index d740aca..25b4715 100644 (file)
--- a/usage
+++ b/usage
@@ -10,17 +10,15 @@ CONFIG
                           can override any of the previous file's settings.
                           (/etc/strapp.conf:~/.strapp/strapp.conf:./strapp.conf)
                           - config settings are overridden by command line opts
-                            except where noted
 
   -j, --client-js=path    Path to the client Strapp code (./client.js)
   -J, --host-js=path      Path to the host Strapp code (./host.js)
-  -T, --no-tls=bool       Don't use HTTPS and WSS protocols (false)
-                          - makes 'ca-cert' and 'ca-key' unnecessary
-
   -C, --ca-cert=path      Accessible location of the CA Cert (../certs/cert.pem)
   -K, --ca-key=path       Accessible location of the CA Key (../certs/key.pem)
-  -p, --port=number       The local port to bind HTTPS listener to (2443)
-  -i, --index=path        File serviced at the root domain (./index.html)
+  -p, --port=number       The local port to bind HTTP/S listener to (2443)
+  -d, --file-dir=path     Path to the directory that will serve files over HTTP
+                          if a client's requested route doesn't exist and can't
+                          be created
 
 ROUTING
   -b, --bind=[string:path[,string:path]]...
@@ -49,6 +47,9 @@ ROUTING
                                 be established, so no remote hosts may exist
 
 COMPATIBILITY
+  --no-tls=bool           Don't use HTTPS and WSS protocols (false)
+                          - makes 'ca-cert' and 'ca-key' unnecessary
+
   --legacy-socket=bool    Use Socket.io compatibility layer to enable 
                           long-polling and AJAX fallbacks (false)
                           - enables optional socket.io dependency