initial commit

This commit is contained in:
AJ ONeal 2015-02-12 09:40:37 +00:00
commit 56b5337d8f
18 changed files with 795 additions and 0 deletions

1
LICENSE Normal file
View File

@ -0,0 +1 @@
Commercial, Proprietary

13
app.js Normal file
View 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
View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
'use strict';
require('../walnut.js');

1
bin/walnut.js Symbolic link
View File

@ -0,0 +1 @@
walnut

5
etc/init.d/install.sh Executable file
View 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
View 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
View 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
View 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;

View 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();
});
});
});
});
};

View 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, ' '));
});
};

View 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
View 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
View 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
View 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
View 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
View 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
View File

34
walnut.js Normal file
View 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');
});