initial commit
This commit is contained in:
		
						commit
						56b5337d8f
					
				
							
								
								
									
										13
									
								
								app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var express = require('express') | ||||||
|  |   ; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (server, host, port, publicDir) { | ||||||
|  |   var app = express() | ||||||
|  |     ; | ||||||
|  | 
 | ||||||
|  |   app.use(express.static(publicDir)); | ||||||
|  | 
 | ||||||
|  |   return app; | ||||||
|  | }; | ||||||
							
								
								
									
										4
									
								
								bin/walnut
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								bin/walnut
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | #!/usr/bin/env node | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | require('../walnut.js'); | ||||||
							
								
								
									
										1
									
								
								bin/walnut.js
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/walnut.js
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | |||||||
|  | walnut | ||||||
							
								
								
									
										5
									
								
								etc/init.d/install.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								etc/init.d/install.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | 
 | ||||||
|  | sudo rsync -v walnut /etc/init.d/ | ||||||
|  | sudo chmod 755 /etc/init.d/walnut | ||||||
|  | sudo update-rc.d walnut defaults | ||||||
							
								
								
									
										39
									
								
								etc/init.d/walnut
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										39
									
								
								etc/init.d/walnut
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | ### BEGIN INIT INFO | ||||||
|  | # Provides:             walnut | ||||||
|  | # Required-Start:       $all | ||||||
|  | # Required-Stop:         | ||||||
|  | # Default-Start:        2 3 4 5 | ||||||
|  | # Default-Stop:         0 1 6 | ||||||
|  | # Short-Description:    WALNUT Home Cloud | ||||||
|  | ### END INIT INFO | ||||||
|  | 
 | ||||||
|  | export PATH=$PATH:/bin:/usr/bin:/usr/local/bin | ||||||
|  | 
 | ||||||
|  | PIDFILE=/var/run/walnut.pid | ||||||
|  | DATE=`date '+%F_%H-%M-%S'` | ||||||
|  | 
 | ||||||
|  | cd /srv/walnut | ||||||
|  | 
 | ||||||
|  | case "$1" in | ||||||
|  |   start) | ||||||
|  |     mkdir -p /srv/walnut/logs | ||||||
|  |     mkdir -p /srv/walnut/.forever | ||||||
|  |     exec forever -p /srv/walnut/.forever --minUptime=20000 --spinSleepTime=100 --workingDir=/srv/walnut/ -l "/srv/walnut/logs/access.${DATE}.log" -e "/srv/walnut/logs/error.${DATE}.log" --pidFile=$PIDFILE start /srv/walnut/bin/walnut.js | ||||||
|  |     ;; | ||||||
|  |   stop) | ||||||
|  |     exec forever stopall | ||||||
|  |     ;; | ||||||
|  |   restart)  | ||||||
|  |     mkdir -p /srv/walnut/logs | ||||||
|  |     mkdir -p /srv/walnut/.forever | ||||||
|  |     exec forever stopall | ||||||
|  |     exec forever -p /srv/walnut/.forever --minUptime=20000 --spinSleepTime=100 --workingDir=/srv/walnut/ -l "/srv/walnut/logs/access.${DATE}.log" -e "/srv/walnut/logs/error.${DATE}.log" --pidFile=$PIDFILE start /srv/walnut/bin/walnut.js | ||||||
|  |     ;; | ||||||
|  |   *) | ||||||
|  | 
 | ||||||
|  |   echo "Usage: /etc/init.d/walnut {start|stop}" | ||||||
|  |   exit 1 | ||||||
|  |   ;; | ||||||
|  | esac | ||||||
|  | 
 | ||||||
|  | exit 0 | ||||||
							
								
								
									
										21
									
								
								etc/init/walnut.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								etc/init/walnut.conf
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | description "WALNUT, by Daplie" | ||||||
|  | version "0.1" | ||||||
|  | author "Daplie Inc" | ||||||
|  | 
 | ||||||
|  | # Upstart has nothing in $PATH by default | ||||||
|  | env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin | ||||||
|  | 
 | ||||||
|  | # Keep the server running on crash or machine reboot | ||||||
|  | respawn | ||||||
|  | respawn limit 10 120 | ||||||
|  | start on runlevel [2345] | ||||||
|  | 
 | ||||||
|  | # Start the server using spark and redirect output to log files | ||||||
|  | script | ||||||
|  |   DATE=`date '+%F_%H-%M-%S'` | ||||||
|  |   cd /srv/walnut | ||||||
|  |   mkdir -p logs | ||||||
|  |   exec node bin/walnut \ | ||||||
|  |     > "./logs/access.${DATE}.log" \ | ||||||
|  |     2> "./logs/error.${DATE}.log" | ||||||
|  | end script | ||||||
							
								
								
									
										84
									
								
								holepunch/beacon.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								holepunch/beacon.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var PromiseA = require('bluebird').Promise | ||||||
|  |   , updateIp = require('./helpers/update-ip.js').update | ||||||
|  |   , request = PromiseA.promisifyAll(require('request')) | ||||||
|  |   , requestAsync = PromiseA.promisify(require('request')) | ||||||
|  |   , upnpForward = require('./helpers/upnp-forward').upnpForward | ||||||
|  |   , pmpForward = require('./helpers/pmp-forward').pmpForward | ||||||
|  |   , loopbackHttps = require('./loopback-https') | ||||||
|  |   //, checkip = require('check-ip-address')
 | ||||||
|  |   ; | ||||||
|  | 
 | ||||||
|  | function openPort(ip, port) { | ||||||
|  |   if (!/tcp|https|http/.test(port.protocol || 'tcp')) { | ||||||
|  |     throw new Error('not yet supported \'' + port.protocol + '\''); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (false === port.testable) { | ||||||
|  |     return PromiseA.resolve(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return loopbackHttps.create(ip, port.private, port.public).then(function () { | ||||||
|  |     console.log('success'); | ||||||
|  |   }).catch(function (err) { | ||||||
|  |     // TODO test err
 | ||||||
|  |     return upnpForward(port).catch(function (err) { | ||||||
|  |       console.error('[ERROR] UPnP Port Forward'); | ||||||
|  |       console.error(err); | ||||||
|  |       // TODO test err
 | ||||||
|  |       return pmpForward(port); | ||||||
|  |     }).then(function () { | ||||||
|  |       return loopbackHttps.create(ip, port.private, port.public); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 1. update dyndns
 | ||||||
|  | // 1.5. check ip every 5 min
 | ||||||
|  | // 2. loopback test on ip for http / https / ssh
 | ||||||
|  | // 3. if needed: discover gateway, map ports
 | ||||||
|  | function beacon(hostnames, ports) { | ||||||
|  |   // test with
 | ||||||
|  |   // dig -p 53 @redirect-www.org pi.nadal.daplie.com A
 | ||||||
|  |   return updateIp({ | ||||||
|  |     updater: 'redirect-www.org' | ||||||
|  |   , port: 65443 | ||||||
|  |   , ddns: hostnames.map(function (hostname) { | ||||||
|  |       return { "name": hostname /*, "value": ipaddress, "type": "A"*/ }; | ||||||
|  |     }) | ||||||
|  |   }).then(function (data) { | ||||||
|  |     var promises = []; | ||||||
|  | 
 | ||||||
|  |     console.log("Updated DynDNS"); | ||||||
|  |     console.log(data); | ||||||
|  |      | ||||||
|  |     ports.forEach(function (port) { | ||||||
|  |       promises.push(openPort(JSON.parse(data)[0].answers[0] || hostname, port)); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return PromiseA.all(promises); | ||||||
|  |   }).then(function () { | ||||||
|  |     console.log('opened ports'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  |   request.getAsync('http://checkip.hellabit.com').spread(function (resp, data) { | ||||||
|  |     console.log("External IP is", data); | ||||||
|  |   }).then(function () { | ||||||
|  |     return upnpForward().catch(function (err) { | ||||||
|  |       console.error('ERROR: UPnP failure:'); | ||||||
|  |       console.error(err); | ||||||
|  |     }); | ||||||
|  |   }).then(function () { | ||||||
|  |     return pmpForward().catch(function (err) { | ||||||
|  |       console.error('TODO: Notify user that their router is not compatible'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // TODO test roundtrip
 | ||||||
|  | */ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //setInterval(beacon, 5 * 60 * 1000);
 | ||||||
|  | exports.run = beacon; | ||||||
							
								
								
									
										57
									
								
								holepunch/helpers/pmp-forward.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								holepunch/helpers/pmp-forward.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var PromiseA = require('bluebird').Promise | ||||||
|  |   , natpmp = require('nat-pmp') | ||||||
|  |   , exec = require('child_process').exec | ||||||
|  |   ; | ||||||
|  | 
 | ||||||
|  | exports.pmpForward = function (port) { | ||||||
|  |   return new PromiseA(function (resolve, reject) { | ||||||
|  |     exec('ip route show default', function (err, stdout, stderr) { | ||||||
|  |       var gw | ||||||
|  |         ; | ||||||
|  | 
 | ||||||
|  |       if (err || stderr) { reject(err || stderr); return; } | ||||||
|  |        | ||||||
|  |       // default via 192.168.1.1 dev eth0
 | ||||||
|  |       gw = stdout.replace(/^default via (\d+\.\d+\.\d+\.\d+) dev[\s\S]+/m, '$1'); | ||||||
|  |       console.log('Possible PMP gateway is', gw); | ||||||
|  | 
 | ||||||
|  |       // create a "client" instance connecting to your local gateway
 | ||||||
|  |       var client = natpmp.connect(gw); | ||||||
|  | 
 | ||||||
|  |       function setPortForward() { | ||||||
|  |         // setup a new port mapping
 | ||||||
|  |         client.portMapping({ | ||||||
|  |           private: port.private || port.public | ||||||
|  |         , public: port.public || port.private | ||||||
|  |         , ttl: port.ttl || 0 // 600
 | ||||||
|  |         }, function (err, info) { | ||||||
|  |           if (err) { | ||||||
|  |             reject(err); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           console.log(info); | ||||||
|  |           // {
 | ||||||
|  |           //   type: 'tcp',
 | ||||||
|  |           //   epoch: 8922109,
 | ||||||
|  |           //   private: 22,
 | ||||||
|  |           //   public: 2222,
 | ||||||
|  |           //   ...
 | ||||||
|  |           // }
 | ||||||
|  |           resolve(); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // explicitly ask for the current external IP address
 | ||||||
|  |       setTimeout(function () { | ||||||
|  |         client.externalIp(function (err, info) { | ||||||
|  |           if (err) throw err; | ||||||
|  |           console.log('Current external IP address: %s', info.ip.join('.')); | ||||||
|  |           setPortForward(); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										45
									
								
								holepunch/helpers/update-ip.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								holepunch/helpers/update-ip.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | |||||||
|  | #!/usr/bin/env node
 | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var PromiseA = require('bluebird').Promise | ||||||
|  |   , https = require('https') | ||||||
|  |   , fs = require('fs') | ||||||
|  |   , path = require('path') | ||||||
|  |   ; | ||||||
|  | 
 | ||||||
|  | module.exports.update = function (opts) { | ||||||
|  |   return new PromiseA(function (resolve, reject) { | ||||||
|  |     var options | ||||||
|  |       , hostname = opts.updater || 'redirect-www.org' | ||||||
|  |       , port = opts.port || 65443 | ||||||
|  |       ; | ||||||
|  | 
 | ||||||
|  |     options = { | ||||||
|  |       host: hostname | ||||||
|  |     , port: port | ||||||
|  |     , method: 'POST' | ||||||
|  |     , headers: { | ||||||
|  |         'Content-Type': 'application/json' | ||||||
|  |       } | ||||||
|  |     , path: '/api/ddns' | ||||||
|  |     , auth: opts.auth || 'admin:secret' | ||||||
|  |     , ca: [ fs.readFileSync(path.join(__dirname, '..', 'certs', 'ca', 'my-root-ca.crt.pem')) ] | ||||||
|  |     }; | ||||||
|  |     options.agent = new https.Agent(options); | ||||||
|  | 
 | ||||||
|  |     https.request(options, function(res) { | ||||||
|  |       var textData = ''; | ||||||
|  | 
 | ||||||
|  |       res.on('error', function (err) { | ||||||
|  |         reject(err); | ||||||
|  |       }); | ||||||
|  |       res.on('data', function (chunk) { | ||||||
|  |         textData += chunk.toString(); | ||||||
|  |         // console.log(chunk.toString());
 | ||||||
|  |       }); | ||||||
|  |       res.on('end', function () { | ||||||
|  |         resolve(textData); | ||||||
|  |       }); | ||||||
|  |     }).end(JSON.stringify(opts.ddns, null, '  ')); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										60
									
								
								holepunch/helpers/upnp-forward.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								holepunch/helpers/upnp-forward.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var PromiseA = require('bluebird').Promise; | ||||||
|  | var natUpnp = require('nat-upnp'); | ||||||
|  | 
 | ||||||
|  | exports.upnpForward = function (port) { | ||||||
|  |   return natUpnp.createClient({ timeout: 1800 }).then(function (client) { | ||||||
|  |     return client.portMapping({ | ||||||
|  |       public: port.public, | ||||||
|  |       private: port.private || port.public, | ||||||
|  |       ttl: port.ttl || 0 | ||||||
|  |     })/*.then(function () { | ||||||
|  |       var promitter = client.getMappings(); | ||||||
|  | 
 | ||||||
|  |       promitter.on('entry', function (entry, i) { | ||||||
|  |         console.log('entry', i); | ||||||
|  |         console.log(entry); | ||||||
|  |       }).then(function (mappings) { | ||||||
|  |         console.log('mappings'); | ||||||
|  |         console.log(mappings); | ||||||
|  |       }); | ||||||
|  |         | ||||||
|  |       return promitter; | ||||||
|  |     })*/; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | client.portUnmapping({ | ||||||
|  |   public: 80 | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | .findGateway().then(function (stuff) { | ||||||
|  |       console.log('[a] gateway'); | ||||||
|  |       console.log(stuff.gateway); | ||||||
|  |       console.log('[a] address'); | ||||||
|  |       console.log(stuff.address); | ||||||
|  |     }).then(function () { | ||||||
|  |       return client | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | client.getMappings({ local: true }, function(err, results) { | ||||||
|  |   console.log('local mappings', results); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | client.externalIp(function(err, ip) { | ||||||
|  |   console.log('ext-ip', ip); | ||||||
|  | }); | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | if (require.main === module) { | ||||||
|  |   exports.upnpForward({ public: 65080, private: 65080, ttl: 0 }).then(function () { | ||||||
|  |     console.log('done'); | ||||||
|  |   }).catch(function (err) { | ||||||
|  |     console.error('ERROR'); | ||||||
|  |     console.error(err); | ||||||
|  |     throw err; | ||||||
|  |   }); | ||||||
|  | } | ||||||
							
								
								
									
										124
									
								
								holepunch/loopback-https.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								holepunch/loopback-https.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var https = require('https'); | ||||||
|  | var path = require('path'); | ||||||
|  | var fs = require('fs'); | ||||||
|  | var PromiseA = global.Promise || require('bluebird').Promise; | ||||||
|  | 
 | ||||||
|  | exports.create = function (ip, localPort, externalPort) { | ||||||
|  |   return new PromiseA(function (resolve, reject) { | ||||||
|  |     var token = Math.random().toString(16).split('.')[1]; | ||||||
|  |     var tokenPath = Math.random().toString(16).split('.')[1]; | ||||||
|  |     var options; | ||||||
|  |     var server; | ||||||
|  |     var options; | ||||||
|  |     var certsPath = path.join(__dirname, 'certs', 'server'); | ||||||
|  |     var caCertsPath = path.join(__dirname, 'certs', 'ca'); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     function testConnection() { | ||||||
|  |       var awesome = false; | ||||||
|  |       var timetok; | ||||||
|  |       var webreq; | ||||||
|  |       var options = { | ||||||
|  |         // not hostname because we set headers.host on our own
 | ||||||
|  |         host: ip | ||||||
|  |       , headers: { | ||||||
|  |           // whatever's on the fake cert
 | ||||||
|  |           'Host': 'redirect-www.org' | ||||||
|  |         } | ||||||
|  |       , port: externalPort | ||||||
|  |       , path: '/' + tokenPath | ||||||
|  |       , ca: fs.readFileSync(path.join(caCertsPath, 'my-root-ca.crt.pem')) | ||||||
|  |       }; | ||||||
|  |       options.agent = new https.Agent(options); | ||||||
|  | 
 | ||||||
|  |       timetok = setTimeout(function () { | ||||||
|  |         reject(new Error("timed out while testing NAT loopback for port " + externalPort)); | ||||||
|  |       }, 2000); | ||||||
|  | 
 | ||||||
|  |       function finishHim(err) { | ||||||
|  |         clearTimeout(timetok); | ||||||
|  |         server.close(function () { | ||||||
|  |           if (!err && awesome) { | ||||||
|  |             resolve(); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (err || !awesome) { | ||||||
|  |           if (err) { | ||||||
|  |             reject(err); | ||||||
|  |           } | ||||||
|  |           else if (!awesome) { | ||||||
|  |             reject(new Error("loopback failed. Why? here's my best guess: " | ||||||
|  |               + "the ssl cert matched, so you've probably got two boxes and this isn't the right one")); | ||||||
|  |           } | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       webreq = https.request(options, function(res) { | ||||||
|  |         res.on('data', function (resToken) { | ||||||
|  |           if (resToken.toString() === token) { | ||||||
|  |             awesome = true; | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |         res.on('error', function (err) { | ||||||
|  |           console.error('[ERROR] https.request.response'); | ||||||
|  |           console.error(err); | ||||||
|  |           finishHim(new Error("loopback failed. Why? here's my best guess: " | ||||||
|  |             + "the connection was interrupted")); | ||||||
|  |           }); | ||||||
|  |         res.on('end', function () { | ||||||
|  |           finishHim(); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       webreq.on('error', function (err) { | ||||||
|  |         console.error('[ERROR] https.request'); | ||||||
|  |         console.error(err); | ||||||
|  |         if (/ssl|cert|chain/i.test(err.message || err.toString())) { | ||||||
|  |           finishHim(new Error("loopback failed. Why? here's my best guess: " | ||||||
|  |             + "the ssl cert validation may have failed (might port-forward to the wrong box)")); | ||||||
|  |         } else { | ||||||
|  |           finishHim(new Error("loopback failed. Why? here's my best guess: " | ||||||
|  |             + "port forwarding isn't configured for " + ip + ":" + externalPort + " to " + localPort)); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       webreq.end(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     //
 | ||||||
|  |     // SSL Certificates
 | ||||||
|  |     //
 | ||||||
|  |     options = { | ||||||
|  |       key: fs.readFileSync(path.join(certsPath, 'my-server.key.pem')) | ||||||
|  |     , ca: [ fs.readFileSync(path.join(caCertsPath, 'my-root-ca.crt.pem')) ] | ||||||
|  |     , cert: fs.readFileSync(path.join(certsPath, 'my-server.crt.pem')) | ||||||
|  |     , requestCert: false | ||||||
|  |     , rejectUnauthorized: false | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     //
 | ||||||
|  |     // Serve an Express App securely with HTTPS
 | ||||||
|  |     //
 | ||||||
|  |     server = https.createServer(options); | ||||||
|  |     function listen(app) { | ||||||
|  |       server.on('request', app); | ||||||
|  |       server.listen(localPort, function () { | ||||||
|  |         localPort = server.address().port; | ||||||
|  |         setTimeout(testConnection, 2000); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     listen(function (req, res) { | ||||||
|  |       if (('/' + tokenPath) === req.url) { | ||||||
|  |         res.end(token); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       res.end('loopback failure'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										50
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | |||||||
|  | { | ||||||
|  |   "name": "walnut", | ||||||
|  |   "version": "0.1.0", | ||||||
|  |   "description": "zero-config home cloud server", | ||||||
|  |   "main": "walnut.js", | ||||||
|  |   "scripts": { | ||||||
|  |     "test": "echo \"Error: no test specified\" && exit 1" | ||||||
|  |   }, | ||||||
|  |   "repository": { | ||||||
|  |     "type": "git", | ||||||
|  |     "url": "https://github.com/Daplie/walnut.git" | ||||||
|  |   }, | ||||||
|  |   "keywords": [ | ||||||
|  |     "ssl", | ||||||
|  |     "tls", | ||||||
|  |     "https", | ||||||
|  |     "rsa", | ||||||
|  |     "pem", | ||||||
|  |     "example", | ||||||
|  |     "demo", | ||||||
|  |     "test", | ||||||
|  |     "openssl", | ||||||
|  |     "crt", | ||||||
|  |     "p12", | ||||||
|  |     "csr", | ||||||
|  |     "certificate", | ||||||
|  |     "certs", | ||||||
|  |     "cert", | ||||||
|  |     "key", | ||||||
|  |     "private", | ||||||
|  |     "public" | ||||||
|  |   ], | ||||||
|  |   "author": "AJ ONeal <aj@daplie.com> (https://daplie.com)", | ||||||
|  |   "license": "Apache2", | ||||||
|  |   "bugs": { | ||||||
|  |     "url": "https://github.com/Daplie/walnut/issues" | ||||||
|  |   }, | ||||||
|  |   "homepage": "https://github.com/Daplie/walnut", | ||||||
|  |   "dependencies": { | ||||||
|  |     "bluebird": "^2.9.6", | ||||||
|  |     "connect": "^3.3.4", | ||||||
|  |     "foreachasync": "^5.0.5", | ||||||
|  |     "ssl-root-cas": "^1.1.7", | ||||||
|  |     "vhost": "^3.0.0", | ||||||
|  |     "bluebird": "^2.9.3", | ||||||
|  |     "check-ip-address": "^1.0.0", | ||||||
|  |     "express": "^4.11.2", | ||||||
|  |     "request": "^2.51.0" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								public/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | <html> | ||||||
|  |   <body> | ||||||
|  | <pre><code>My name is Marvin, not that you would care. | ||||||
|  | 
 | ||||||
|  | You've reached {{host}} on port {{port}}. Meh... Congratulations, I guess."</code></pre> | ||||||
|  |     <script> | ||||||
|  |       document.body.innerHTML = document.body.innerHTML | ||||||
|  |         .replace(/{{(\s+)?host(\s+)?}}/, location.hostname) | ||||||
|  |         .replace(/{{(\s+)?port(\s+)?}}/, location.host.replace(/.*:/, '')) | ||||||
|  |         ; | ||||||
|  |     </script> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										102
									
								
								serve.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										102
									
								
								serve.js
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,102 @@ | |||||||
|  | #!/usr/bin/env node
 | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var https = require('https') | ||||||
|  |   , http = require('http') | ||||||
|  |   , path = require('path') | ||||||
|  |   , port = process.argv[2] || 65443 | ||||||
|  |   , insecurePort = process.argv[3] || 65080 | ||||||
|  |   , fs = require('fs') | ||||||
|  |   , path = require('path') | ||||||
|  |   , checkip = require('check-ip-address') | ||||||
|  |   , server | ||||||
|  |   , insecureServer | ||||||
|  |   , options | ||||||
|  |   , certsPath = path.join(__dirname, 'certs', 'server') | ||||||
|  |   , caCertsPath = path.join(__dirname, 'certs', 'ca') | ||||||
|  |   ; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | //
 | ||||||
|  | // SSL Certificates
 | ||||||
|  | //
 | ||||||
|  | options = { | ||||||
|  |   key: fs.readFileSync(path.join(certsPath, 'my-server.key.pem')) | ||||||
|  | , ca: [ fs.readFileSync(path.join(caCertsPath, 'my-root-ca.crt.pem')) ] | ||||||
|  | , cert: fs.readFileSync(path.join(certsPath, 'my-server.crt.pem')) | ||||||
|  | , requestCert: false | ||||||
|  | , rejectUnauthorized: false | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | //
 | ||||||
|  | // Serve an Express App securely with HTTPS
 | ||||||
|  | //
 | ||||||
|  | server = https.createServer(options); | ||||||
|  | checkip.getExternalIp().then(function (ip) { | ||||||
|  |   var host = ip || 'local.helloworld3000.com' | ||||||
|  |     ; | ||||||
|  | 
 | ||||||
|  |   function listen(app) { | ||||||
|  |     server.on('request', app); | ||||||
|  |     server.listen(port, function () { | ||||||
|  |       port = server.address().port; | ||||||
|  |       console.log('Listening on https://127.0.0.1:' + port); | ||||||
|  |       console.log('Listening on https://local.helloworld3000.com:' + port); | ||||||
|  |       if (ip) { | ||||||
|  |         console.log('Listening on https://' + ip + ':' + port); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var publicDir = path.join(__dirname, 'public'); | ||||||
|  |   var app = require('./app').create(server, host, port, publicDir); | ||||||
|  |   listen(app); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | //
 | ||||||
|  | // Redirect HTTP ot HTTPS
 | ||||||
|  | //
 | ||||||
|  | // This simply redirects from the current insecure location to the encrypted location
 | ||||||
|  | //
 | ||||||
|  | insecureServer = http.createServer(); | ||||||
|  | insecureServer.on('request', function (req, res) { | ||||||
|  |   var newLocation = 'https://' | ||||||
|  |     + req.headers.host.replace(/:\d+/, ':' + port) + req.url | ||||||
|  |     ; | ||||||
|  | 
 | ||||||
|  |   var metaRedirect = '' | ||||||
|  |     + '<html>\n' | ||||||
|  |     + '<head>\n' | ||||||
|  |     + '  <style>* { background-color: white; color: white; text-decoration: none; }</style>\n' | ||||||
|  |     + '  <META http-equiv="refresh" content="0;URL=' + newLocation + '">\n' | ||||||
|  |     + '</head>\n' | ||||||
|  |     + '<body style="display: none;">\n' | ||||||
|  |     + '  <p>You requested an insecure resource. Please use this instead: \n' | ||||||
|  |     + '    <a href="' + newLocation + '">' + newLocation + '</a></p>\n' | ||||||
|  |     + '</body>\n' | ||||||
|  |     + '</html>\n' | ||||||
|  |     ; | ||||||
|  | 
 | ||||||
|  |   // DO NOT HTTP REDIRECT
 | ||||||
|  |   /* | ||||||
|  |   res.setHeader('Location', newLocation); | ||||||
|  |   res.statusCode = 302; | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  |   // BAD NEWS BEARS
 | ||||||
|  |   //
 | ||||||
|  |   // When people are experimenting with the API and posting tutorials
 | ||||||
|  |   // they'll use cURL and they'll forget to prefix with https://
 | ||||||
|  |   // If we allow that, then many users will be sending private tokens
 | ||||||
|  |   // and such with POSTs in clear text and, worse, it will work!
 | ||||||
|  |   // To minimize this, we give browser users a mostly optimal experience,
 | ||||||
|  |   // but people experimenting with the API get a message letting them know
 | ||||||
|  |   // that they're doing it wrong and thus forces them to ensure they encrypt.
 | ||||||
|  |   res.setHeader('Content-Type', 'text/html'); | ||||||
|  |   res.end(metaRedirect); | ||||||
|  | }); | ||||||
|  | insecureServer.listen(insecurePort, function(){ | ||||||
|  |   console.log("\nRedirecting all http traffic to https\n"); | ||||||
|  | }); | ||||||
							
								
								
									
										142
									
								
								vhost-sni-server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								vhost-sni-server.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,142 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var https           = require('https') | ||||||
|  |   , http            = require('http') | ||||||
|  |   , PromiseA        = require('bluebird').Promise | ||||||
|  |   , forEachAsync    = require('foreachasync').forEachAsync.create(PromiseA) | ||||||
|  |   , fs              = require('fs') | ||||||
|  |   , path            = require('path') | ||||||
|  |   , crypto          = require('crypto') | ||||||
|  |   , connect         = require('connect') | ||||||
|  |   , vhost           = require('vhost') | ||||||
|  | 
 | ||||||
|  |   // connect / express app
 | ||||||
|  |   , app             = connect() | ||||||
|  | 
 | ||||||
|  |   // SSL Server
 | ||||||
|  |   , secureContexts  = {} | ||||||
|  |   , secureOpts | ||||||
|  |   , secureServer | ||||||
|  |   , securePort      = /*process.argv[2] ||*/ 443 | ||||||
|  | 
 | ||||||
|  |   // force SSL upgrade server
 | ||||||
|  |   , insecureServer | ||||||
|  |   , insecurePort    = /*process.argv[3] ||*/ 80 | ||||||
|  | 
 | ||||||
|  |   // the ssl domains I have
 | ||||||
|  |   // TODO read vhosts minus 
 | ||||||
|  |   , domains         = fs.readdirSync(path.join(__dirname, 'vhosts')).filter(function (node) { | ||||||
|  |                         // not a hidden or private file
 | ||||||
|  |                         return '.' !== node[0] && '_' !== node[0]; | ||||||
|  |                       }) | ||||||
|  |   ; | ||||||
|  | 
 | ||||||
|  | require('ssl-root-cas') | ||||||
|  |   .inject() | ||||||
|  |   ; | ||||||
|  | 
 | ||||||
|  | function getAppContext(domain) { | ||||||
|  |   var localApp | ||||||
|  |     ; | ||||||
|  | 
 | ||||||
|  |   localApp = require(path.join(__dirname, 'vhosts', domain, 'app.js')); | ||||||
|  |   if (localApp.create) { | ||||||
|  |     // TODO read local config.yml and pass it in
 | ||||||
|  |     localApp = localApp.create(/*config*/); | ||||||
|  |   } | ||||||
|  |   if (!localApp.then) { | ||||||
|  |     localApp = PromiseA.resolve(localApp); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return localApp; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | forEachAsync(domains, function (domain) { | ||||||
|  |   secureContexts[domain] = crypto.createCredentials({ | ||||||
|  |     key:  fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/server/my-server.key.pem')) | ||||||
|  |   , cert: fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/server/my-server.crt.pem')) | ||||||
|  |   , ca:   fs.readdirSync(path.join(__dirname, 'vhosts', domain, 'certs/ca')).map(function (node) { | ||||||
|  |             return fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/ca', node)); | ||||||
|  |           }) | ||||||
|  |   }).context; | ||||||
|  | 
 | ||||||
|  |   return getAppContext(domain).then(function (localApp) { | ||||||
|  |     app.use(vhost('www.' + domain, localApp)); | ||||||
|  |     app.use(vhost(domain, localApp)); | ||||||
|  |   }); | ||||||
|  | }).then(function () { | ||||||
|  |   // fallback / default domain
 | ||||||
|  |   /* | ||||||
|  |   app.use('/', function (req, res) { | ||||||
|  |     res.statusCode = 404; | ||||||
|  |     res.end("<html><body><h1>Hello, World... This isn't the domain you're looking for.</h1></body></html>"); | ||||||
|  |   }); | ||||||
|  |   */ | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | //provide a SNICallback when you create the options for the https server
 | ||||||
|  | secureOpts = { | ||||||
|  |   //SNICallback is passed the domain name, see NodeJS docs on TLS
 | ||||||
|  |   SNICallback:  function (domain) { | ||||||
|  |                   //console.log('SNI:', domain);
 | ||||||
|  |                   return secureContexts[domain]; | ||||||
|  |                 } | ||||||
|  |                 // fallback / default domain
 | ||||||
|  | , key:          fs.readFileSync(path.join(__dirname, 'certs/server', 'dummy-server.key.pem')) | ||||||
|  | , cert:         fs.readFileSync(path.join(__dirname, 'certs/server', 'dummy-server.crt.pem')) | ||||||
|  | , ca:           fs.readdirSync(path.join(__dirname, 'certs/ca')).map(function (node) { | ||||||
|  |                   return fs.readFileSync(path.join(__dirname, 'certs/ca', node)); | ||||||
|  |                 }) | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | secureServer = https.createServer(secureOpts); | ||||||
|  | secureServer.on('request', app); | ||||||
|  | secureServer.listen(securePort, function () { | ||||||
|  |   console.log("Listening on https://localhost:" + secureServer.address().port); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | //
 | ||||||
|  | // Redirect HTTP ot HTTPS
 | ||||||
|  | //
 | ||||||
|  | // This simply redirects from the current insecure location to the encrypted location
 | ||||||
|  | //
 | ||||||
|  | insecureServer = http.createServer(); | ||||||
|  | insecureServer.on('request', function (req, res) { | ||||||
|  |   var newLocation = 'https://' | ||||||
|  |     + req.headers.host.replace(/:\d+/, ':' + port) + req.url | ||||||
|  |     ; | ||||||
|  | 
 | ||||||
|  |   var metaRedirect = '' | ||||||
|  |     + '<html>\n' | ||||||
|  |     + '<head>\n' | ||||||
|  |     + '  <style>* { background-color: white; color: white; text-decoration: none; }</style>\n' | ||||||
|  |     + '  <META http-equiv="refresh" content="0;URL=' + newLocation + '">\n' | ||||||
|  |     + '</head>\n' | ||||||
|  |     + '<body style="display: none;">\n' | ||||||
|  |     + '  <p>You requested an insecure resource. Please use this instead: \n' | ||||||
|  |     + '    <a href="' + newLocation + '">' + newLocation + '</a></p>\n' | ||||||
|  |     + '</body>\n' | ||||||
|  |     + '</html>\n' | ||||||
|  |     ; | ||||||
|  | 
 | ||||||
|  |   // DO NOT HTTP REDIRECT
 | ||||||
|  |   /* | ||||||
|  |   res.setHeader('Location', newLocation); | ||||||
|  |   res.statusCode = 302; | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  |   // BAD NEWS BEARS
 | ||||||
|  |   //
 | ||||||
|  |   // When people are experimenting with the API and posting tutorials
 | ||||||
|  |   // they'll use cURL and they'll forget to prefix with https://
 | ||||||
|  |   // If we allow that, then many users will be sending private tokens
 | ||||||
|  |   // and such with POSTs in clear text and, worse, it will work!
 | ||||||
|  |   // To minimize this, we give browser users a mostly optimal experience,
 | ||||||
|  |   // but people experimenting with the API get a message letting them know
 | ||||||
|  |   // that they're doing it wrong and thus forces them to ensure they encrypt.
 | ||||||
|  |   res.setHeader('Content-Type', 'text/html'); | ||||||
|  |   res.end(metaRedirect); | ||||||
|  | }); | ||||||
|  | insecureServer.listen(insecurePort, function(){ | ||||||
|  |   console.log("\nRedirecting all http traffic to https\n"); | ||||||
|  | }); | ||||||
							
								
								
									
										0
									
								
								vhosts/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								vhosts/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										34
									
								
								walnut.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								walnut.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | var holepunch = require('./holepunch/beacon'); | ||||||
|  | var config = require('./device.json') | ||||||
|  | var ports ; | ||||||
|  | 
 | ||||||
|  | ports = [ | ||||||
|  |   { private: 22 | ||||||
|  |   , public: 65022 | ||||||
|  |   , protocol: 'tcp' | ||||||
|  |   , ttl: 0 | ||||||
|  |   , test: { service: 'ssh' } | ||||||
|  |   , testable: false | ||||||
|  |   } | ||||||
|  | , { private: 65443 | ||||||
|  |   , public: 65443 | ||||||
|  |   , protocol: 'tcp' | ||||||
|  |   , ttl: 0 | ||||||
|  |   , test: { service: 'https' } | ||||||
|  |   } | ||||||
|  | , { private: 65080 | ||||||
|  |   , public: 65080 | ||||||
|  |   , protocol: 'tcp' | ||||||
|  |   , ttl: 0 | ||||||
|  |   , test: { service: 'http' } | ||||||
|  |   } | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | holepunch.run([ | ||||||
|  |   'aj.daplie.com' | ||||||
|  | , 'coolaj86.com' | ||||||
|  | , 'prod.coolaj86.com' | ||||||
|  | , 'production.coolaj86.com' | ||||||
|  | ], ports).then(function () { | ||||||
|  |   require('./vhost-sni-server.js'); | ||||||
|  | }); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user