Compare commits

..

51 Commits

Author SHA1 Message Date
AJ ONeal dccebfe16b Merge branch 'v1' 2018-05-16 02:22:09 -06:00
AJ ONeal a87e69e332 update urls 2018-05-16 02:21:05 -06:00
AJ ONeal 8fb910ddf9 Update 'installer/install.sh' 2018-04-10 04:44:23 +00:00
AJ ONeal 158892f88c rebrand 2018-04-10 04:42:47 +00:00
AJ ONeal e462978154 install service before chown 2017-12-11 22:24:26 +00:00
AJ ONeal 3a7e4cd2ab don't pull on detached head 2017-12-11 22:14:04 +00:00
AJ ONeal 4f16f92208 update urls and version 2017-12-11 22:03:22 +00:00
Drew Warren 34dff39358 Update install.sh oauth3 ver to tag instead of version branch 2017-11-14 13:41:50 -07:00
AJ ONeal 136431d493 Merge branch 'v1.1' 2017-11-10 16:32:30 -07:00
AJ ONeal 4b9e07842d remove cruft 2017-11-10 16:32:25 -07:00
AJ ONeal 43105ba266 Merge branch 'v1.1' 2017-11-10 16:28:23 -07:00
AJ ONeal add6745475 Merge branch 'master' of git.daplie.com:Daplie/goldilocks.js 2017-11-10 16:27:47 -07:00
AJ ONeal 2969eb3247 Merge branch 'v1.1' of git.daplie.com:Daplie/goldilocks.js into v1.1 2017-11-10 12:45:42 -07:00
AJ ONeal 2c6e5cfa46 update urls 2017-11-10 12:28:40 -07:00
AJ ONeal 037c4df6e0 Uninstall bins & services vs config 2017-11-08 14:21:07 -07:00
tigerbot dd7bc74dad v1.1.5 2017-11-08 14:17:40 -07:00
tigerbot 12c2fd1819 Merge branch 'dns-challenge' 2017-11-08 14:17:25 -07:00
AJ ONeal a8aedcbc31 Delete test-chain.sh 2017-11-08 14:14:41 -07:00
AJ ONeal ea010427e8 Delete terms.sh 2017-11-08 14:14:06 -07:00
tigerbot d8cc8fe8e6 fixed a few places ddns module.disabled wasn't handle properly 2017-11-08 12:08:36 -07:00
tigerbot 11f2d37044 implemented dns-01 ACME challenges 2017-11-08 12:05:38 -07:00
tigerbot 40bd1d9cc6 moved some functions into a utils files for wider use within ddns 2017-11-07 16:42:00 -07:00
AJ ONeal 2277b22d9d Merge branch 'v1.1' 2017-11-07 16:19:56 -07:00
AJ ONeal b6b9d5f2f3 Merge branch 'v1.1' 2017-11-07 16:12:13 -07:00
AJ ONeal 0a233cfcf0 Merge branch 'v1.1' 2017-11-07 16:08:34 -07:00
AJ ONeal dd035219a3 Merge branch 'v1.1' 2017-11-07 16:02:07 -07:00
tigerbot 57f97eebdb removed `le-challenge-ddns` from package.json 2017-11-07 15:59:06 -07:00
AJ ONeal 0024d51289 Merge branch 'v1' 2017-11-07 15:45:46 -07:00
AJ ONeal 1382b8b4e2 Merge branch 'v1' 2017-11-07 15:07:01 -07:00
AJ ONeal 828712bf12 Merge branch 'v1.1' of git.daplie.com:Daplie/goldilocks.js into v1.1 2017-11-07 15:06:38 -07:00
AJ ONeal ac36a35c19 Merge branch 'installer-v2' 2017-11-07 15:02:57 -07:00
AJ ONeal 8c34316979 Merge branch 'installer-v2' 2017-11-07 14:28:51 -07:00
AJ ONeal 65920f8fce Merge branch 'installer-v2' 2017-11-07 21:02:02 +00:00
AJ ONeal 75d2680830 Merge branch 'installer-v2' 2017-11-07 20:59:17 +00:00
AJ ONeal 0b464cab36 Merge branch 'installer-v2' 2017-11-07 20:55:31 +00:00
AJ ONeal 35016cd124 Merge branch 'master' of ssh://git.daplie.com/Daplie/goldilocks.js 2017-11-07 20:52:53 +00:00
AJ ONeal 4b2e6b1600 Merge branch 'master' of git.daplie.com:Daplie/goldilocks.js 2017-11-07 13:41:56 -07:00
tigerbot e071b8c3eb v1.1.4 2017-11-07 10:32:34 -07:00
tigerbot 307d81690d Merge branch 'reorganize-modules' 2017-11-06 18:09:37 -07:00
tigerbot 2f06c7fbdc fixed socks5 running on start if specified in config 2017-11-06 18:06:37 -07:00
tigerbot b324016056 made the loopback check more robust 2017-11-01 11:40:56 -06:00
tigerbot eda766e48c moved tunnel client manager into DDNS directory where it's used 2017-10-31 18:10:46 -06:00
tigerbot a27252eb77 made tunnel server respond to config changes 2017-10-31 15:39:24 -06:00
tigerbot 7423d6065f added config for the tunnel server to the schema 2017-10-31 12:14:48 -06:00
tigerbot 9ec642237c fixed error changing setting in mDNS 2017-10-30 16:00:35 -06:00
tigerbot 16589e65f6 moved most things related to TCP connections to a tcp directory 2017-10-30 15:57:18 -06:00
tigerbot 9a63f30bf2 fixed incorrect behavior when loopback or tunnel initially fails 2017-10-30 14:00:27 -06:00
tigerbot c697008573 made the mDNS module able to adapt to changes in config 2017-10-30 14:00:27 -06:00
tigerbot c132861cab made TCP binding and forwarding modules respond to config changes 2017-10-30 14:00:21 -06:00
tigerbot c637671c78 added ability to detect config changes to the socks5 module 2017-10-26 16:55:16 -06:00
tigerbot 5534ba2ef1 moved the handling of udp stuff to a separate file 2017-10-26 16:27:10 -06:00
27 changed files with 926 additions and 2895 deletions

View File

@ -1,3 +1,10 @@
v1.1.5 - Implemented dns-01 ACME challenges
v1.1.4 - Improved responsiveness to config updates
* changed which TCP/UDP ports are bound to on config update
* update tunnel server settings on config update
* update socks5 setting on config update
v1.1.3 - Better late than never... here's some stuff we've got v1.1.3 - Better late than never... here's some stuff we've got
* fixed (probably) network settings not being readable * fixed (probably) network settings not being readable
* supports timeouts in loopback check * supports timeouts in loopback check

View File

@ -23,13 +23,13 @@ Install Standalone
### curl | bash ### curl | bash
```bash ```bash
curl -fsSL https://git.daplie.com/Daplie/goldilocks.js/raw/v1.1/installer/get.sh | bash curl -fsSL https://git.coolaj86.com/coolaj86/goldilocks.js/raw/v1.1/installer/get.sh | bash
``` ```
### git ### git
```bash ```bash
git clone https://git.daplie.com/Daplie/goldilocks.js git clone https://git.coolaj86.com/coolaj86/goldilocks.js
pushd goldilocks.js pushd goldilocks.js
git checkout v1.1 git checkout v1.1
bash installer/install.sh bash installer/install.sh
@ -39,10 +39,10 @@ bash installer/install.sh
```bash ```bash
# v1 in git (unauthenticated) # v1 in git (unauthenticated)
npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js#v1 npm install -g git+https://git@git.coolaj86.com:coolaj86/goldilocks.js#v1
# v1 in git (via ssh) # v1 in git (via ssh)
npm install -g git+ssh://git@git.daplie.com:Daplie/goldilocks.js#v1 npm install -g git+ssh://git@git.coolaj86.com:coolaj86/goldilocks.js#v1
# v1 in npm # v1 in npm
npm install -g goldilocks@v1 npm install -g goldilocks@v1
@ -50,8 +50,16 @@ npm install -g goldilocks@v1
### Uninstall ### Uninstall
Remove goldilocks and services:
``` ```
rm -rf /srv/goldilocks/ /var/goldilocks/ /etc/goldilocks/ /opt/goldilocks/ /var/log/goldilocks/ /etc/tmpfiles.d/goldilocks.conf /etc/systemd/system/goldilocks.service /etc/ssl/goldilocks rm -rf /opt/goldilocks/ /srv/goldilocks/ /var/goldilocks/ /var/log/goldilocks/ /etc/tmpfiles.d/goldilocks.conf /etc/systemd/system/goldilocks.service
```
Remove config as well
```
rm -rf /etc/goldilocks/ /etc/ssl/goldilocks
``` ```
Usage Usage
@ -73,7 +81,7 @@ We have service support for
* launchd (macOS) * launchd (macOS)
```bash ```bash
curl https://git.daplie.com/Daplie/goldilocks.js/raw/master/install.sh | bash curl https://git.coolaj86.com/coolaj86/goldilocks.js/raw/master/install.sh | bash
``` ```
Modules & Configuration Modules & Configuration
@ -297,6 +305,12 @@ tls:
challenge_type: 'http-01' challenge_type: 'http-01'
``` ```
**NOTE:** If you specify `dns-01` as the challenge type there must also be a
[DDNS module](#ddns) defined for all of the relevant domains (though not all
domains handled by a single TLS module need to be handled by the same DDNS
module). The DDNS module provides all of the information needed to actually
set the DNS records needed to verify ownership.
### tcp ### tcp
The tcp system handles both *raw* and *tls-terminated* tcp network traffic The tcp system handles both *raw* and *tls-terminated* tcp network traffic
@ -398,7 +412,7 @@ sni = vpn.example.com
connect = example.com:443 connect = example.com:443
``` ```
3) [Use stunnel.js](https://git.daplie.com/Daplie/node-tunnel-client) as described in the "tunnel_server" section below. 3) [Use stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js) as described in the "tunnel_server" section below.
### tcp.forward ### tcp.forward
@ -602,7 +616,7 @@ mdns:
You can discover goldilocks with `mdig`. You can discover goldilocks with `mdig`.
``` ```
npm install -g git+https://git.daplie.com/Daplie/mdig.git npm install -g git+https://git.coolaj86.com/coolaj86/mdig.js.git
mdig _cloud._tcp.local mdig _cloud._tcp.local
``` ```
@ -631,7 +645,7 @@ TODO
* [ ] http - redirect based on domain name (not just path) * [ ] http - redirect based on domain name (not just path)
* [ ] tcp - bind should be able to specify localhost, uniquelocal, private, or ip * [ ] tcp - bind should be able to specify localhost, uniquelocal, private, or ip
* [ ] tcp - if destination host is omitted default to localhost, if dst port is missing, default to src * [ ] tcp - if destination host is omitted default to localhost, if dst port is missing, default to src
* [ ] sys - `curl https://daplie.me/goldilocks | bash -s example.com` * [ ] sys - `curl https://coolaj86.com/goldilocks | bash -s example.com`
* [ ] oauth3 - `example.com/.well-known/domains@oauth3.org/directives.json` * [ ] oauth3 - `example.com/.well-known/domains@oauth3.org/directives.json`
* [ ] oauth3 - commandline questionnaire * [ ] oauth3 - commandline questionnaire
* [x] modules - use consistent conventions (i.e. address vs host + port) * [x] modules - use consistent conventions (i.e. address vs host + port)

View File

@ -7,7 +7,7 @@ my_ver=master
my_tmp=$(mktemp -d) my_tmp=$(mktemp -d)
mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name
git clone https://git.daplie.com/Daplie/goldilocks.js.git $my_tmp/opt/$my_name/lib/node_modules/$my_name git clone https://git.coolaj86.com/coolaj86/goldilocks.js.git $my_tmp/opt/$my_name/lib/node_modules/$my_name
echo "Installing to $my_tmp (will be moved after install)" echo "Installing to $my_tmp (will be moved after install)"
pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name

View File

@ -5,7 +5,7 @@
# # # #
############################### ###############################
# See https://git.daplie.com/Daplie/daplie-snippets/blob/master/bash/http-get.sh # See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh
_h_http_get="" _h_http_get=""
_h_http_opts="" _h_http_opts=""

View File

@ -11,11 +11,13 @@ sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_service" > "$my_app_dis
sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_service.2" > "$my_app_dist/$my_app_systemd_service" sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_service.2" > "$my_app_dist/$my_app_systemd_service"
rm "$my_app_dist/$my_app_systemd_service.2" rm "$my_app_dist/$my_app_systemd_service.2"
safe_copy_config "$my_app_dist/$my_app_systemd_service" "$my_root/$my_app_systemd_service" safe_copy_config "$my_app_dist/$my_app_systemd_service" "$my_root/$my_app_systemd_service"
$sudo_cmd chown root:root "$my_root/$my_app_systemd_service"
sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_tmpfiles" > "$my_app_dist/$my_app_systemd_tmpfiles.2" sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_tmpfiles" > "$my_app_dist/$my_app_systemd_tmpfiles.2"
sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_tmpfiles.2" > "$my_app_dist/$my_app_systemd_tmpfiles" sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_tmpfiles.2" > "$my_app_dist/$my_app_systemd_tmpfiles"
rm "$my_app_dist/$my_app_systemd_tmpfiles.2" rm "$my_app_dist/$my_app_systemd_tmpfiles.2"
safe_copy_config "$my_app_dist/$my_app_systemd_tmpfiles" "$my_root/$my_app_systemd_tmpfiles" safe_copy_config "$my_app_dist/$my_app_systemd_tmpfiles" "$my_root/$my_app_systemd_tmpfiles"
$sudo_cmd chown root:root "$my_root/$my_app_systemd_tmpfiles"
$sudo_cmd systemctl stop "${my_app_name}.service" >/dev/null 2>/dev/null || true $sudo_cmd systemctl stop "${my_app_name}.service" >/dev/null 2>/dev/null || true
$sudo_cmd systemctl daemon-reload $sudo_cmd systemctl daemon-reload

View File

@ -7,10 +7,10 @@ set -u
### IMPORTANT ### ### IMPORTANT ###
### VERSION ### ### VERSION ###
my_name=goldilocks my_name=goldilocks
my_app_pkg_name=com.daplie.goldilocks.web my_app_pkg_name=com.coolaj86.goldilocks.web
my_app_ver="v1.1" my_app_ver="v1.1"
my_azp_oauth3_ver="v1.2" my_azp_oauth3_ver="v1.2.3"
export NODE_VERSION="v8.9.0" export NODE_VERSION="v8.9.3"
if [ -z "${my_tmp-}" ]; then if [ -z "${my_tmp-}" ]; then
my_tmp="$(mktemp -d)" my_tmp="$(mktemp -d)"
@ -29,7 +29,7 @@ my_npm="$NPM_CONFIG_PREFIX/bin/npm"
my_app_dist=$my_tmp/opt/$my_name/lib/node_modules/$my_name/dist my_app_dist=$my_tmp/opt/$my_name/lib/node_modules/$my_name/dist
installer_base="https://git.daplie.com/Daplie/goldilocks.js/raw/$my_app_ver" installer_base="https://git.coolaj86.com/coolaj86/goldilocks.js/raw/$my_app_ver"
# Backwards compat # Backwards compat
# some scripts still use the old names # some scripts still use the old names
@ -71,13 +71,13 @@ pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name
$my_npm install $my_npm install
popd popd
pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name/packages/assets pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name/packages/assets
OAUTH3_GIT_URL="https://git.daplie.com/Oauth3/oauth3.js.git" OAUTH3_GIT_URL="https://git.oauth3.org/OAuth3/oauth3.js.git"
git clone ${OAUTH3_GIT_URL} oauth3.org || true git clone ${OAUTH3_GIT_URL} oauth3.org || true
ln -s oauth3.org org.oauth3 ln -s oauth3.org org.oauth3
pushd oauth3.org pushd oauth3.org
git remote set-url origin ${OAUTH3_GIT_URL} git remote set-url origin ${OAUTH3_GIT_URL}
git checkout $my_azp_oauth3_ver git checkout $my_azp_oauth3_ver
git pull #git pull
popd popd
mkdir -p jquery.com mkdir -p jquery.com
@ -119,6 +119,8 @@ set -e
source ./installer/my-user-my-group.sh source ./installer/my-user-my-group.sh
echo "User $my_user Group $my_group" echo "User $my_user Group $my_group"
source ./installer/install-system-service.sh
$sudo_cmd chown -R $my_user:$my_group $my_tmp/* $sudo_cmd chown -R $my_user:$my_group $my_tmp/*
$sudo_cmd chown root:root $my_tmp/* $sudo_cmd chown root:root $my_tmp/*
$sudo_cmd chown root:root $my_tmp $sudo_cmd chown root:root $my_tmp
@ -126,7 +128,6 @@ $sudo_cmd chmod 0755 $my_tmp
# don't change permissions on /, /etc, etc # don't change permissions on /, /etc, etc
$sudo_cmd rsync -a --ignore-existing $my_tmp/ $my_root/ $sudo_cmd rsync -a --ignore-existing $my_tmp/ $my_root/
$sudo_cmd rsync -a --ignore-existing $my_app_dist/etc/$my_name/$my_name.yml $my_root/etc/$my_name/$my_name.yml $sudo_cmd rsync -a --ignore-existing $my_app_dist/etc/$my_name/$my_name.yml $my_root/etc/$my_name/$my_name.yml
source ./installer/install-system-service.sh
# Change to admin perms # Change to admin perms
$sudo_cmd chown -R $my_user:$my_group $my_root/opt/$my_name $sudo_cmd chown -R $my_user:$my_group $my_root/opt/$my_name

View File

@ -174,6 +174,14 @@ var mdnsSchema = {
} }
}; };
var tunnelSvrSchema = {
type: 'object'
, properties: {
servernames: { type: 'array', items: { type: 'string' }}
, secret: { type: 'string' }
}
};
var ddnsSchema = { var ddnsSchema = {
type: 'object' type: 'object'
, properties: { , properties: {
@ -223,6 +231,7 @@ var mainSchema = {
, ddns: ddnsSchema , ddns: ddnsSchema
, socks5: socks5Schema , socks5: socks5Schema
, device: deviceSchema , device: deviceSchema
, tunnel_server: tunnelSvrSchema
} }
, additionalProperties: false , additionalProperties: false
}; };

View File

@ -0,0 +1,122 @@
'use strict';
// Much of this file was based on the `le-challenge-ddns` library (which we are not using
// here because it's method of setting records requires things we don't really want).
module.exports.create = function (deps, conf, utils) {
function getReleventSessionId(domain) {
var sessId;
utils.iterateAllModules(function (mod, domainList) {
// We return a truthy value in these cases because of the way the iterate function
// handles modules grouped by domain. By returning true we are saying these domains
// are "handled" and so if there are multiple modules we won't be given the rest.
if (sessId) { return true; }
if (domainList.indexOf(domain) < 0) { return true; }
// But if the domains are relevant but we don't know how to handle the module we
// return false to allow us to look at any other modules that might exist here.
if (mod.type !== 'dns@oauth3.org') { return false; }
sessId = mod.tokenId || mod.token_id;
return true;
});
return sessId;
}
function get(args, domain, challenge, done) {
done(new Error("Challenge.get() does not need an implementation for dns-01. (did you mean Challenge.loopback?)"));
}
// same as get, but external
function loopback(args, domain, challenge, done) {
var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain;
require('dns').resolveTxt(challengeDomain, done);
}
var activeChallenges = {};
async function removeAsync(args, domain) {
var data = activeChallenges[domain];
if (!data) {
console.warn(new Error('cannot remove DNS challenge for ' + domain + ': already removed'));
return;
}
var session = await utils.getSession(data.sessId);
var directives = await deps.OAUTH3.discover(session.token.aud);
var apiOpts = {
api: 'dns.unset'
, session: session
, type: 'TXT'
, value: data.keyAuthDigest
};
await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, data.splitDomain));
delete activeChallenges[domain];
}
async function setAsync(args, domain, challenge, keyAuth) {
if (activeChallenges[domain]) {
await removeAsync(args, domain, challenge);
}
var sessId = getReleventSessionId(domain);
if (!sessId) {
throw new Error('no DDNS module handles the domain ' + domain);
}
var session = await utils.getSession(sessId);
var directives = await deps.OAUTH3.discover(session.token.aud);
// I'm not sure what role challenge is supposed to play since even in the library
// this code is based on it was never used, but check for it anyway because ...
if (!challenge || keyAuth) {
console.warn(new Error('DDNS challenge missing challenge or keyAuth'));
}
var keyAuthDigest = require('crypto').createHash('sha256').update(keyAuth || '').digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain;
var splitDomain = (await utils.splitDomains(directives.api, [challengeDomain]))[0];
var apiOpts = {
api: 'dns.set'
, session: session
, type: 'TXT'
, value: keyAuthDigest
, ttl: args.ttl || 0
};
await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, splitDomain));
activeChallenges[domain] = {
sessId
, keyAuthDigest
, splitDomain
};
return new Promise(res => setTimeout(res, 1000));
}
// It might be slightly easier to use arguments and apply, but the library that will use
// this function counts the arguments we expect.
function set(a, b, c, d, done) {
setAsync(a, b, c, d).then(result => done(null, result), done);
}
function remove(a, b, c, done) {
removeAsync(a, b, c).then(result => done(null, result), done);
}
function getOptions() {
return {
oauth3: 'oauth3.org'
, debug: conf.debug
, acmeChallengeDns: '_acme-challenge.'
};
}
return {
getOptions
, set
, get
, remove
, loopback
};
};

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
module.exports.create = function (deps, conf) { module.exports.create = function (deps, conf, utils) {
function dnsType(addr) { function dnsType(addr) {
if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) { if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) {
return 'A'; return 'A';
@ -10,62 +10,6 @@ module.exports.create = function (deps, conf) {
} }
} }
var tldCache = {};
async function getTlds(provider) {
async function updateCache() {
var reqObj = {
url: deps.OAUTH3.url.normalize(provider)+'/api/com.daplie.domains/prices'
, method: 'GET'
, json: true
};
var resp = await deps.OAUTH3.request(reqObj);
var tldObj = {};
resp.data.forEach(function (tldInfo) {
if (tldInfo.enabled) {
tldObj[tldInfo.tld] = true;
}
});
tldCache[provider] = {
time: Date.now()
, tlds: tldObj
};
return tldObj;
}
// If we've never cached the results we need to return the promise that will fetch the recult,
// otherwise we can return the cached value. If the cached value has "expired", we can still
// return the cached value we just want to update the cache in parellel (making sure we only
// update once).
if (!tldCache[provider]) {
return updateCache();
}
if (!tldCache[provider].updating && Date.now() - tldCache[provider].time > 24*60*60*1000) {
tldCache[provider].updating = true;
updateCache();
}
return tldCache[provider].tlds;
}
async function splitDomains(provider, domains) {
var tlds = await getTlds(provider);
return domains.map(function (domain) {
var split = domain.split('.');
var tldSegCnt = tlds[split.slice(-2).join('.')] ? 2 : 1;
// Currently assuming that the sld can't contain dots, and that the tld can have at
// most one dot. Not 100% sure this is a valid assumption, but exceptions should be
// rare even if the assumption isn't valid.
return {
tld: split.slice(-tldSegCnt).join('.')
, sld: split.slice(-tldSegCnt-1, -tldSegCnt).join('.')
, sub: split.slice(0, -tldSegCnt-1).join('.')
};
});
}
async function setDeviceAddress(session, addr, domains) { async function setDeviceAddress(session, addr, domains) {
var directives = await deps.OAUTH3.discover(session.token.aud); var directives = await deps.OAUTH3.discover(session.token.aud);
@ -111,7 +55,7 @@ module.exports.create = function (deps, conf) {
return goodAddrDomains.indexOf(domain) < 0; return goodAddrDomains.indexOf(domain) < 0;
}); });
var oldDns = await splitDomains(directives.api, badAddrDomains); var oldDns = await utils.splitDomains(directives.api, badAddrDomains);
var common = { var common = {
api: 'devices.detach' api: 'devices.detach'
, session: session , session: session
@ -124,7 +68,7 @@ module.exports.create = function (deps, conf) {
console.log('removed bad DNS records for ' + badAddrDomains.join(', ')); console.log('removed bad DNS records for ' + badAddrDomains.join(', '));
} }
var newDns = await splitDomains(directives.api, requiredUpdates); var newDns = await utils.splitDomains(directives.api, requiredUpdates);
common = { common = {
api: 'devices.attach' api: 'devices.attach'
, session: session , session: session
@ -169,7 +113,7 @@ module.exports.create = function (deps, conf) {
async function removeDomains(session, domains) { async function removeDomains(session, domains) {
var directives = await deps.OAUTH3.discover(session.token.aud); var directives = await deps.OAUTH3.discover(session.token.aud);
var oldDns = await splitDomains(directives.api, domains); var oldDns = await utils.splitDomains(directives.api, domains);
var common = { var common = {
api: 'devices.detach' api: 'devices.detach'
, session: session , session: session

View File

@ -3,48 +3,21 @@
module.exports.create = function (deps, conf) { module.exports.create = function (deps, conf) {
var dns = deps.PromiseA.promisifyAll(require('dns')); var dns = deps.PromiseA.promisifyAll(require('dns'));
var network = deps.PromiseA.promisifyAll(deps.recase.camelCopy(require('network'))); var network = deps.PromiseA.promisifyAll(deps.recase.camelCopy(require('network')));
var loopback = require('./loopback').create(deps, conf);
var dnsCtrl = require('./dns-ctrl').create(deps, conf);
var equal = require('deep-equal'); var equal = require('deep-equal');
var utils = require('./utils').create(deps, conf);
var loopback = require('./loopback').create(deps, conf, utils);
var dnsCtrl = require('./dns-ctrl').create(deps, conf, utils);
var challenge = require('./challenge-responder').create(deps, conf, utils);
var tunnelClients = require('./tunnel-client-manager').create(deps, conf, utils);
var loopbackDomain; var loopbackDomain;
function iterateAllModules(action, curConf) {
curConf = curConf || conf;
var promises = curConf.ddns.modules.map(function (mod) {
return action(mod, mod.domains);
});
curConf.domains.forEach(function (dom) {
if (!dom.modules || !Array.isArray(dom.modules.ddns) || !dom.modules.ddns.length) {
return null;
}
// For the time being all of our things should only be tried once (regardless if it succeeded)
// TODO: revisit this behavior when we support multiple ways of setting records, and/or
// if we want to allow later modules to run if early modules fail.
promises.push(dom.modules.ddns.reduce(function (prom, mod) {
if (prom) { return prom; }
return action(mod, dom.names);
}, null));
});
return deps.PromiseA.all(promises.filter(Boolean));
}
async function getSession(id) {
var session = await deps.storage.tokens.get(id);
if (!session) {
throw new Error('no user token with ID "'+id+'"');
}
return session;
}
var tunnelActive = false; var tunnelActive = false;
async function startTunnel(tunnelSession, mod, domainList) { async function startTunnel(tunnelSession, mod, domainList) {
try { try {
var dnsSession = await getSession(mod.tokenId); var dnsSession = await utils.getSession(mod.tokenId);
var tunnelDomain = await deps.tunnelClients.start(tunnelSession || dnsSession, domainList); var tunnelDomain = await tunnelClients.start(tunnelSession || dnsSession, domainList);
var addrList; var addrList;
try { try {
@ -59,7 +32,9 @@ module.exports.create = function (deps, conf) {
throw new Error('failed to lookup IP for tunnel domain "' + tunnelDomain + '"'); throw new Error('failed to lookup IP for tunnel domain "' + tunnelDomain + '"');
} }
if (!mod.disabled) {
await dnsCtrl.setDeviceAddress(dnsSession, addrList[0], domainList); await dnsCtrl.setDeviceAddress(dnsSession, addrList[0], domainList);
}
} catch (err) { } catch (err) {
console.log('error starting tunnel for', domainList.join(', ')); console.log('error starting tunnel for', domainList.join(', '));
console.log(err); console.log(err);
@ -73,7 +48,7 @@ module.exports.create = function (deps, conf) {
tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId); tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId);
} }
await iterateAllModules(function (mod, domainList) { await utils.iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return null; } if (mod.type !== 'dns@oauth3.org') { return null; }
return startTunnel(tunnelSession, mod, domainList); return startTunnel(tunnelSession, mod, domainList);
@ -82,14 +57,14 @@ module.exports.create = function (deps, conf) {
tunnelActive = true; tunnelActive = true;
} }
async function disconnectTunnels() { async function disconnectTunnels() {
deps.tunnelClients.disconnect(); tunnelClients.disconnect();
tunnelActive = false; tunnelActive = false;
await Promise.resolve(); await Promise.resolve();
} }
async function checkTunnelTokens() { async function checkTunnelTokens() {
var oldTokens = deps.tunnelClients.current(); var oldTokens = tunnelClients.current();
var newTokens = await iterateAllModules(function checkTokens(mod, domainList) { var newTokens = await utils.iterateAllModules(function checkTokens(mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return null; } if (mod.type !== 'dns@oauth3.org') { return null; }
var domainStr = domainList.slice().sort().join(','); var domainStr = domainList.slice().sort().join(',');
@ -103,7 +78,7 @@ module.exports.create = function (deps, conf) {
} }
}); });
await Promise.all(Object.values(oldTokens).map(deps.tunnelClients.remove)); await Promise.all(Object.values(oldTokens).map(tunnelClients.remove));
if (!newTokens.length) { return; } if (!newTokens.length) { return; }
@ -187,10 +162,10 @@ module.exports.create = function (deps, conf) {
} }
publicAddress = addr; publicAddress = addr;
await iterateAllModules(function setModuleDNS(mod, domainList) { await utils.iterateAllModules(function setModuleDNS(mod, domainList) {
if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; } if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; }
return getSession(mod.tokenId).then(function (session) { return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.setDeviceAddress(session, addr, domainList); return dnsCtrl.setDeviceAddress(session, addr, domainList);
}).catch(function (err) { }).catch(function (err) {
console.log('error setting DNS records for', domainList.join(', ')); console.log('error setting DNS records for', domainList.join(', '));
@ -205,13 +180,13 @@ module.exports.create = function (deps, conf) {
// this returns a Promise, but since the functions we use are synchronous // this returns a Promise, but since the functions we use are synchronous
// and change our enclosed variables we don't need to wait for the return. // and change our enclosed variables we don't need to wait for the return.
iterateAllModules(function (mod, domainList) { utils.iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return; } if (mod.type !== 'dns@oauth3.org') { return; }
prevMods[mod.id] = { mod, domainList }; prevMods[mod.id] = { mod, domainList };
return true; return true;
}, prevConf); }, prevConf);
iterateAllModules(function (mod, domainList) { utils.iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return; } if (mod.type !== 'dns@oauth3.org') { return; }
curMods[mod.id] = { mod, domainList }; curMods[mod.id] = { mod, domainList };
@ -234,8 +209,11 @@ module.exports.create = function (deps, conf) {
// Then remove DNS records for the domains that we are no longer responsible for. // Then remove DNS records for the domains that we are no longer responsible for.
await Promise.all(Object.values(prevMods).map(function ({mod, domainList}) { await Promise.all(Object.values(prevMods).map(function ({mod, domainList}) {
// If the module was disabled before there should be any records that we need to clean up
if (mod.disabled) { return; }
var oldDomains; var oldDomains;
if (!curMods[mod.id] || mod.tokenId !== curMods[mod.id].mod.tokenId) { if (!curMods[mod.id] || curMods[mod.id].disabled || mod.tokenId !== curMods[mod.id].mod.tokenId) {
oldDomains = domainList.slice(); oldDomains = domainList.slice();
} else { } else {
oldDomains = domainList.filter(function (domain) { oldDomains = domainList.filter(function (domain) {
@ -249,7 +227,7 @@ module.exports.create = function (deps, conf) {
return; return;
} }
return getSession(mod.tokenId).then(function (session) { return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.removeDomains(session, oldDomains); return dnsCtrl.removeDomains(session, oldDomains);
}); });
}).filter(Boolean)); }).filter(Boolean));
@ -259,6 +237,9 @@ module.exports.create = function (deps, conf) {
// And add DNS records for any newly added domains. // And add DNS records for any newly added domains.
await Promise.all(Object.values(curMods).map(function ({mod, domainList}) { await Promise.all(Object.values(curMods).map(function ({mod, domainList}) {
// Don't set any new records if the module has been disabled.
if (mod.disabled) { return; }
var newDomains; var newDomains;
if (!prevMods[mod.id] || mod.tokenId !== prevMods[mod.id].mod.tokenId) { if (!prevMods[mod.id] || mod.tokenId !== prevMods[mod.id].mod.tokenId) {
newDomains = domainList.slice(); newDomains = domainList.slice();
@ -274,7 +255,7 @@ module.exports.create = function (deps, conf) {
return; return;
} }
return getSession(mod.tokenId).then(function (session) { return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.setDeviceAddress(session, publicAddress, newDomains); return dnsCtrl.setDeviceAddress(session, publicAddress, newDomains);
}); });
}).filter(Boolean)); }).filter(Boolean));
@ -340,5 +321,6 @@ module.exports.create = function (deps, conf) {
, getDeviceAddresses: dnsCtrl.getDeviceAddresses , getDeviceAddresses: dnsCtrl.getDeviceAddresses
, recheckPubAddr: recheckPubAddr , recheckPubAddr: recheckPubAddr
, updateConf: updateConf , updateConf: updateConf
, challenge
}; };
}; };

View File

@ -6,6 +6,52 @@ module.exports.create = function (deps, config) {
var activeTunnels = {}; var activeTunnels = {};
var activeDomains = {}; var activeDomains = {};
var customNet = {
createConnection: function (opts, cb) {
console.log('[gl.tunnel] creating connection');
// here "reader" means the socket that looks like the connection being accepted
// here "writer" means the remote-looking part of the socket that driving the connection
var writer;
function usePair(err, reader) {
if (err) {
process.nextTick(function () {
writer.emit('error', err);
});
return;
}
var wrapOpts = Object.assign({localAddress: '127.0.0.2', localPort: 'tunnel-0'}, opts);
wrapOpts.firstChunk = opts.data;
wrapOpts.hyperPeek = !!opts.data;
// Also override the remote and local address info. We use `defineProperty` because
// otherwise we run into problems of setting properties with only getters defined.
Object.defineProperty(reader, 'remoteAddress', { value: wrapOpts.remoteAddress });
Object.defineProperty(reader, 'remotePort', { value: wrapOpts.remotePort });
Object.defineProperty(reader, 'remoteFamiliy', { value: wrapOpts.remoteFamiliy });
Object.defineProperty(reader, 'localAddress', { value: wrapOpts.localAddress });
Object.defineProperty(reader, 'localPort', { value: wrapOpts.localPort });
Object.defineProperty(reader, 'localFamiliy', { value: wrapOpts.localFamiliy });
deps.tcp.handler(reader, wrapOpts);
process.nextTick(function () {
// this cb will cause the stream to emit its (actually) first data event
// (even though it already gave a peek into that first data chunk)
console.log('[tunnel] callback, data should begin to flow');
cb();
});
}
// We used to use `stream-pair` for non-tls connections, but there are places
// that require properties/functions to be present on the socket that aren't
// present on a JSStream so it caused problems.
writer = require('socket-pair').create(usePair);
return writer;
}
};
function fillData(data) { function fillData(data) {
if (typeof data === 'string') { if (typeof data === 'string') {
data = { jwt: data }; data = { jwt: data };
@ -70,7 +116,7 @@ module.exports.create = function (deps, config) {
// get the promise that should tell us more about if it worked or not. // get the promise that should tell us more about if it worked or not.
activeTunnels[data.tunnelUrl] = stunnel.connect({ activeTunnels[data.tunnelUrl] = stunnel.connect({
stunneld: data.tunnelUrl stunneld: data.tunnelUrl
, net: deps.tunnel.net , net: customNet
// NOTE: the ports here aren't that important since we are providing a custom // NOTE: the ports here aren't that important since we are providing a custom
// `net.createConnection` that doesn't actually use the port. What is important // `net.createConnection` that doesn't actually use the port. What is important
// is that any services we are interested in are listed in this object and have // is that any services we are interested in are listed in this object and have

102
lib/ddns/utils.js Normal file
View File

@ -0,0 +1,102 @@
'use strict';
module.exports.create = function (deps, conf) {
async function getSession(id) {
var session = await deps.storage.tokens.get(id);
if (!session) {
throw new Error('no user token with ID "' + id + '"');
}
return session;
}
function iterateAllModules(action, curConf) {
curConf = curConf || conf;
var promises = [];
curConf.domains.forEach(function (dom) {
if (!dom.modules || !Array.isArray(dom.modules.ddns) || !dom.modules.ddns.length) {
return null;
}
// For the time being all of our things should only be tried once (regardless if it succeeded)
// TODO: revisit this behavior when we support multiple ways of setting records, and/or
// if we want to allow later modules to run if early modules fail.
promises.push(dom.modules.ddns.reduce(function (prom, mod) {
if (prom) { return prom; }
return action(mod, dom.names);
}, null));
});
curConf.ddns.modules.forEach(function (mod) {
promises.push(action(mod, mod.domains));
});
return Promise.all(promises.filter(Boolean));
}
var tldCache = {};
async function updateTldCache(provider) {
var reqObj = {
url: deps.OAUTH3.url.normalize(provider) + '/api/com.daplie.domains/prices'
, method: 'GET'
, json: true
};
var resp = await deps.OAUTH3.request(reqObj);
var tldObj = {};
resp.data.forEach(function (tldInfo) {
if (tldInfo.enabled) {
tldObj[tldInfo.tld] = true;
}
});
tldCache[provider] = {
time: Date.now()
, tlds: tldObj
};
return tldObj;
}
async function getTlds(provider) {
// If we've never cached the results we need to return the promise that will fetch the result,
// otherwise we can return the cached value. If the cached value has "expired", we can still
// return the cached value we just want to update the cache in parellel (making sure we only
// update once).
if (!tldCache[provider]) {
tldCache[provider] = {
updating: true
, tlds: updateTldCache(provider)
};
}
if (!tldCache[provider].updating && Date.now() - tldCache[provider].time > 24 * 60 * 60 * 1000) {
tldCache[provider].updating = true;
updateTldCache(provider);
}
return tldCache[provider].tlds;
}
async function splitDomains(provider, domains) {
var tlds = await getTlds(provider);
return domains.map(function (domain) {
var split = domain.split('.');
var tldSegCnt = tlds[split.slice(-2).join('.')] ? 2 : 1;
// Currently assuming that the sld can't contain dots, and that the tld can have at
// most one dot. Not 100% sure this is a valid assumption, but exceptions should be
// rare even if the assumption isn't valid.
return {
tld: split.slice(-tldSegCnt).join('.')
, sld: split.slice(-tldSegCnt - 1, -tldSegCnt).join('.')
, sub: split.slice(0, -tldSegCnt - 1).join('.')
};
});
}
return {
getSession
, iterateAllModules
, getTlds
, splitDomains
};
};

View File

@ -1,303 +0,0 @@
'use strict';
module.exports.create = function (deps, config) {
console.log('config', config);
//var PromiseA = global.Promise;
var PromiseA = require('bluebird');
var listeners = require('./servers').listeners;
var domainUtils = require('./domain-utils');
var modules;
var addrProperties = [
'remoteAddress'
, 'remotePort'
, 'remoteFamily'
, 'localAddress'
, 'localPort'
, 'localFamily'
];
function nameMatchesDomains(name, domainList) {
return domainList.some(function (pattern) {
return domainUtils.match(pattern, name);
});
}
function loadModules() {
modules = {};
modules.tls = require('./modules/tls').create(deps, config, tcpHandler);
modules.http = require('./modules/http').create(deps, config, modules.tls.middleware);
}
function checkTcpProxy(conn, opts) {
var proxied = false;
// TCP Proxying (ie forwarding based on domain name not incoming port) only works for
// TLS wrapped connections, so if the opts don't give us a servername or don't tell us
// this is the decrypted side of a TLS connection we can't handle it here.
if (!opts.servername || !opts.encrypted) { return proxied; }
function proxy(mod) {
// First thing we need to add to the connection options is where to proxy the connection to
var newConnOpts = domainUtils.separatePort(mod.address || '');
newConnOpts.port = newConnOpts.port || mod.port;
newConnOpts.host = newConnOpts.host || mod.host || 'localhost';
// Then we add all of the connection address information. We need to prefix all of the
// properties with '_' so we can provide the information to any connection `createConnection`
// implementation but not have the default implementation try to bind the same local port.
addrProperties.forEach(function (name) {
newConnOpts['_' + name] = opts[name] || opts['_'+name] || conn[name] || conn['_'+name];
});
deps.proxy(conn, newConnOpts);
return true;
}
proxied = config.domains.some(function (dom) {
if (!dom.modules || !Array.isArray(dom.modules.tcp)) { return false; }
if (!nameMatchesDomains(opts.servername, dom.names)) { return false; }
return dom.modules.tcp.some(function (mod) {
if (mod.type !== 'proxy') { return false; }
return proxy(mod);
});
});
proxied = proxied || config.tcp.modules.some(function (mod) {
if (mod.type !== 'proxy') { return false; }
if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; }
return proxy(mod);
});
return proxied;
}
// opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
function peek(conn, firstChunk, opts) {
if (!modules) {
loadModules();
}
opts.firstChunk = firstChunk;
conn.__opts = opts;
// TODO port/service-based routing can do here
// TLS byte 1 is handshake and byte 6 is client hello
if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) {
modules.tls.emit('connection', conn);
return;
}
// This doesn't work with TLS, but now that we know this isn't a TLS connection we can
// unshift the first chunk back onto the connection for future use. The unshift should
// happen after any listeners are attached to it but before any new data comes in.
if (!opts.hyperPeek) {
process.nextTick(function () {
conn.unshift(firstChunk);
});
}
// Connection is not TLS, check for HTTP next.
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
var firstStr = firstChunk.toString();
if (/HTTP\//i.test(firstStr)) {
modules.http.emit('connection', conn);
return;
}
}
console.warn('failed to identify protocol from first chunk', firstChunk);
conn.destroy();
}
function tcpHandler(conn, opts) {
function getProp(name) {
return opts[name] || opts['_'+name] || conn[name] || conn['_'+name];
}
opts = opts || {};
var logName = getProp('remoteAddress') + ':' + getProp('remotePort') + ' -> ' +
getProp('localAddress') + ':' + getProp('localPort');
console.log('[tcpHandler]', logName, 'connection started - encrypted: ' + (opts.encrypted || false));
var start = Date.now();
conn.on('timeout', function () {
console.log('[tcpHandler]', logName, 'connection timed out', (Date.now()-start)/1000);
});
conn.on('end', function () {
console.log('[tcpHandler]', logName, 'connection ended', (Date.now()-start)/1000);
});
conn.on('close', function () {
console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000);
});
if (checkTcpProxy(conn, opts)) { return; }
// XXX PEEK COMMENT XXX
// TODO we can have our cake and eat it too
// we can skip the need to wrap the TLS connection twice
// because we've already peeked at the data,
// but this needs to be handled better before we enable that
// (because it creates new edge cases)
if (opts.hyperPeek) {
console.log('hyperpeek');
peek(conn, opts.firstChunk, opts);
return;
}
function onError(err) {
console.error('[error] socket errored peeking -', err);
conn.destroy();
}
conn.once('error', onError);
conn.once('data', function (chunk) {
conn.removeListener('error', onError);
peek(conn, chunk, opts);
});
}
function udpHandler(port, msg) {
if (!Array.isArray(config.udp.modules)) {
return;
}
var socket = require('dgram').createSocket('udp4');
config.udp.modules.forEach(function (mod) {
if (mod.type !== 'forward') {
console.warn('found bad DNS module', mod);
return;
}
if (mod.ports.indexOf(port) < 0) {
return;
}
var dest = require('./domain-utils').separatePort(mod.address || '');
dest.port = dest.port || mod.port;
dest.host = dest.host || mod.host || 'localhost';
socket.send(msg, dest.port, dest.host);
});
}
function createTcpForwarder(mod) {
var dest = require('./domain-utils').separatePort(mod.address || '');
dest.port = dest.port || mod.port;
dest.host = dest.host || mod.host || 'localhost';
return function (conn) {
var newConnOpts = {};
addrProperties.forEach(function (name) {
newConnOpts['_'+name] = conn[name];
});
deps.proxy(conn, Object.assign(newConnOpts, dest));
};
}
deps.tunnel = deps.tunnel || {};
deps.tunnel.net = {
createConnection: function (opts, cb) {
console.log('[gl.tunnel] creating connection');
// here "reader" means the socket that looks like the connection being accepted
// here "writer" means the remote-looking part of the socket that driving the connection
var writer;
var wrapOpts = {};
function usePair(err, reader) {
if (err) {
process.nextTick(function () {
writer.emit('error', err);
});
return;
}
// this has the normal net/tcp stuff plus our custom stuff
// opts = { address, port,
// hostname, servername, tls, encrypted, data, localAddress, localPort, remoteAddress, remotePort, remoteFamily }
Object.keys(opts).forEach(function (key) {
wrapOpts[key] = opts[key];
try {
reader[key] = opts[key];
} catch(e) {
// can't set real socket getters, like remoteAddr
}
});
// A few more extra specialty options
wrapOpts.localAddress = wrapOpts.localAddress || '127.0.0.2'; // TODO use the tunnel's external address
wrapOpts.localPort = wrapOpts.localPort || 'tunnel-0';
try {
reader._remoteAddress = wrapOpts.remoteAddress;
reader._remotePort = wrapOpts.remotePort;
reader._remoteFamily = wrapOpts.remoteFamily;
reader._localAddress = wrapOpts.localAddress;
reader._localPort = wrapOpts.localPort;
reader._localFamily = wrapOpts.localFamily;
} catch(e) {
}
tcpHandler(reader, wrapOpts);
process.nextTick(function () {
// this cb will cause the stream to emit its (actually) first data event
// (even though it already gave a peek into that first data chunk)
console.log('[tunnel] callback, data should begin to flow');
cb();
});
}
wrapOpts.firstChunk = opts.data;
wrapOpts.hyperPeek = !!opts.data;
// We used to use `stream-pair` for non-tls connections, but there are places
// that require properties/functions to be present on the socket that aren't
// present on a JSStream so it caused problems.
writer = require('socket-pair').create(usePair);
return writer;
}
};
deps.tunnelClients = require('./tunnel-client-manager').create(deps, config);
deps.tunnelServer = require('./tunnel-server-manager').create(deps, config);
var listenPromises = [];
var tcpPortMap = {};
config.tcp.bind.filter(Number).forEach(function (port) {
tcpPortMap[port] = true;
});
(config.tcp.modules || []).forEach(function (mod) {
if (mod.type === 'forward') {
var forwarder = createTcpForwarder(mod);
mod.ports.forEach(function (port) {
if (!tcpPortMap[port]) {
console.log("forwarding port", port, "that wasn't specified in bind");
} else {
delete tcpPortMap[port];
}
listenPromises.push(listeners.tcp.add(port, forwarder));
});
}
else if (mod.type !== 'proxy') {
console.warn('unknown TCP module specified', mod);
}
});
var portList = Object.keys(tcpPortMap).map(Number).sort();
portList.forEach(function (port) {
listenPromises.push(listeners.tcp.add(port, tcpHandler));
});
if (config.udp.bind) {
config.udp.bind.forEach(function (port) {
listenPromises.push(listeners.udp.add(port, udpHandler.bind(port)));
});
}
if (!config.mdns.disabled) {
require('./mdns').start(deps, config, portList[0]);
}
return PromiseA.all(listenPromises);
};

View File

@ -2,6 +2,7 @@
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var queryName = '_cloud._tcp.local'; var queryName = '_cloud._tcp.local';
var dnsSuite = require('dns-suite');
function createResponse(name, ownerIds, packet, ttl, mainPort) { function createResponse(name, ownerIds, packet, ttl, mainPort) {
var rpacket = { var rpacket = {
@ -85,20 +86,19 @@ function createResponse(name, ownerIds, packet, ttl, mainPort) {
}); });
}); });
return require('dns-suite').DNSPacket.write(rpacket); return dnsSuite.DNSPacket.write(rpacket);
} }
module.exports.start = function (deps, config, mainPort) { module.exports.create = function (deps, config) {
var socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true }); var socket;
var dns = require('dns-suite');
var nextBroadcast = -1; var nextBroadcast = -1;
socket.on('message', function (message, rinfo) { function handlePacket(message, rinfo) {
// console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port); // console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port);
var packet; var packet;
try { try {
packet = dns.DNSPacket.parse(message); packet = dnsSuite.DNSPacket.parse(message);
} }
catch (er) { catch (er) {
// `dns-suite` actually errors on a lot of the packets floating around in our network, // `dns-suite` actually errors on a lot of the packets floating around in our network,
@ -108,16 +108,12 @@ module.exports.start = function (deps, config, mainPort) {
} }
// Only respond to queries. // Only respond to queries.
if (packet.header.qr !== 0) { if (packet.header.qr !== 0) { return; }
return;
}
// Only respond if they were asking for cloud devices. // Only respond if they were asking for cloud devices.
if (packet.question.length !== 1 || packet.question[0].name !== queryName) { if (packet.question.length !== 1) { return; }
return; if (packet.question[0].name !== queryName) { return; }
} if (packet.question[0].typeName !== 'PTR') { return; }
if (packet.question[0].typeName !== 'PTR' || packet.question[0].className !== 'IN' ) { if (packet.question[0].className !== 'IN' ) { return; }
return;
}
var proms = [ var proms = [
deps.storage.mdnsId.get() deps.storage.mdnsId.get()
@ -131,7 +127,7 @@ module.exports.start = function (deps, config, mainPort) {
]; ];
PromiseA.all(proms).then(function (results) { PromiseA.all(proms).then(function (results) {
var resp = createResponse(results[0], results[1], packet, config.mdns.ttl, mainPort); var resp = createResponse(results[0], results[1], packet, config.mdns.ttl, deps.tcp.mainPort);
var now = Date.now(); var now = Date.now();
if (now > nextBroadcast) { if (now > nextBroadcast) {
socket.send(resp, config.mdns.port, config.mdns.broadcast); socket.send(resp, config.mdns.port, config.mdns.broadcast);
@ -140,7 +136,14 @@ module.exports.start = function (deps, config, mainPort) {
socket.send(resp, rinfo.port, rinfo.address); socket.send(resp, rinfo.port, rinfo.address);
} }
}); });
}); }
function start() {
socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true });
socket.on('message', handlePacket);
return new Promise(function (resolve, reject) {
socket.once('error', reject);
socket.bind(config.mdns.port, function () { socket.bind(config.mdns.port, function () {
var addr = this.address(); var addr = this.address();
@ -153,5 +156,48 @@ module.exports.start = function (deps, config, mainPort) {
// much more difficult for someone to use us as part of a DDoS attack by // much more difficult for someone to use us as part of a DDoS attack by
// spoofing the UDP address a request came from. // spoofing the UDP address a request came from.
socket.setTTL(1); socket.setTTL(1);
socket.removeListener('error', reject);
resolve();
}); });
});
}
function stop() {
return new Promise(function (resolve, reject) {
socket.once('error', reject);
socket.close(function () {
socket.removeListener('error', reject);
socket = null;
resolve();
});
});
}
function updateConf() {
var promise;
if (config.mdns.disabled) {
if (socket) {
promise = stop();
}
} else {
if (!socket) {
promise = start();
} else if (socket.address().port !== config.mdns.port) {
promise = stop().then(start);
} else {
// Can't check membership, so just add the current broadcast address to make sure
// it's set. If it's already set it will throw an exception (at least on linux).
try {
socket.addMembership(config.mdns.broadcast);
} catch (e) {}
promise = Promise.resolve();
}
}
}
updateConf();
return {
updateConf
};
}; };

View File

@ -10,20 +10,16 @@ module.exports.addTcpListener = function (port, handler) {
if (stat) { if (stat) {
if (stat._closing) { if (stat._closing) {
module.exports.destroyTcpListener(port); stat.server.destroy();
} } else {
else if (handler !== stat.handler) { // We're already listening on the port, so we only have 2 options. We can either
// replace the handler or reject with an error. (Though neither is really needed
// we'll replace the current listener // if the handlers are the same). Until there is reason to do otherwise we are
// opting for the replacement.
stat.handler = handler; stat.handler = handler;
resolve(); resolve();
return; return;
} }
else {
// this exact listener is already open
resolve();
return;
}
} }
var enableDestroy = require('server-destroy'); var enableDestroy = require('server-destroy');
@ -34,7 +30,7 @@ module.exports.addTcpListener = function (port, handler) {
stat = serversMap[port] = { stat = serversMap[port] = {
server: server server: server
, handler: handler , handler: handler
, _closing: null , _closing: false
}; };
// Add .destroy so we can close all open connections. Better if added before listen // Add .destroy so we can close all open connections. Better if added before listen
@ -66,14 +62,24 @@ module.exports.addTcpListener = function (port, handler) {
}); });
}); });
}; };
module.exports.closeTcpListener = function (port) { module.exports.closeTcpListener = function (port, timeout) {
return new PromiseA(function (resolve) { return new PromiseA(function (resolve) {
var stat = serversMap[port]; var stat = serversMap[port];
if (!stat) { if (!stat) {
resolve(); resolve();
return; return;
} }
stat.server.once('close', resolve); stat._closing = true;
var timeoutId;
if (timeout) {
timeoutId = setTimeout(() => stat.server.destroy(), timeout);
}
stat.server.once('close', function () {
clearTimeout(timeoutId);
resolve();
});
stat.server.close(); stat.server.close();
}); });
}; };
@ -84,7 +90,9 @@ module.exports.destroyTcpListener = function (port) {
} }
}; };
module.exports.listTcpListeners = function () { module.exports.listTcpListeners = function () {
return Object.keys(serversMap).map(Number).filter(Boolean); return Object.keys(serversMap).map(Number).filter(function (port) {
return port && !serversMap[port]._closing;
});
}; };

View File

@ -63,15 +63,29 @@ module.exports.create = function (deps, config) {
}); });
} }
if (config.socks5 && config.socks5.enabled) { var configEnabled = false;
function updateConf() {
var wanted = config.socks5 && config.socks5.enabled;
if (configEnabled && !wanted) {
stop().catch(function (err) {
console.error('failed to stop socks5 proxy on config change', err);
});
configEnabled = false;
}
if (wanted && !configEnabled) {
start(config.socks5.port).catch(function (err) { start(config.socks5.port).catch(function (err) {
console.error('failed to start Socks5 proxy', err); console.error('failed to start Socks5 proxy', err);
}); });
configEnabled = true;
} }
}
process.nextTick(updateConf);
return { return {
curState: curState curState
, start: start , start
, stop: stop , stop
, updateConf
}; };
}; };

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
module.exports.create = function (deps, conf, greenlockMiddleware) { module.exports.create = function (deps, conf, tcpMods) {
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var statAsync = PromiseA.promisify(require('fs').stat); var statAsync = PromiseA.promisify(require('fs').stat);
var domainMatches = require('../domain-utils').match; var domainMatches = require('../domain-utils').match;
@ -162,8 +162,8 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
return false; return false;
} }
if (deps.tunnelServer.isClientDomain(separatePort(headers.host).host)) { if (deps.stunneld.isClientDomain(separatePort(headers.host).host)) {
deps.tunnelServer.handleClientConn(conn); deps.stunneld.handleClientConn(conn);
process.nextTick(function () { process.nextTick(function () {
conn.unshift(opts.firstChunk); conn.unshift(opts.firstChunk);
conn.resume(); conn.resume();
@ -172,7 +172,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
} }
if (!acmeServer) { if (!acmeServer) {
acmeServer = require('http').createServer(greenlockMiddleware); acmeServer = require('http').createServer(tcpMods.tls.middleware);
} }
return emitConnection(acmeServer, conn, opts); return emitConnection(acmeServer, conn, opts);
} }
@ -214,8 +214,8 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
return emitConnection(adminServer, conn, opts); return emitConnection(adminServer, conn, opts);
} }
if (deps.tunnelServer.isAdminDomain(host)) { if (deps.stunneld.isAdminDomain(host)) {
deps.tunnelServer.handleAdminConn(conn); deps.stunneld.handleAdminConn(conn);
process.nextTick(function () { process.nextTick(function () {
conn.unshift(opts.firstChunk); conn.unshift(opts.firstChunk);
conn.resume(); conn.resume();
@ -241,7 +241,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
res.statusCode = 502; res.statusCode = 502;
res.setHeader('Connection', 'close'); res.setHeader('Connection', 'close');
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');
res.end(require('../proxy-conn').getRespBody(err, conf.debug)); res.end(tcpMods.proxy.getRespBody(err, conf.debug));
}); });
proxyServer = http.createServer(function (req, res) { proxyServer = http.createServer(function (req, res) {
@ -292,7 +292,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
newConnOpts.remoteAddress = opts.address || conn.remoteAddress; newConnOpts.remoteAddress = opts.address || conn.remoteAddress;
newConnOpts.remotePort = opts.port || conn.remotePort; newConnOpts.remotePort = opts.port || conn.remotePort;
deps.proxy(conn, newConnOpts, opts.firstChunk); tcpMods.proxy(conn, newConnOpts, opts.firstChunk);
} }
function checkProxy(mod, conn, opts, headers) { function checkProxy(mod, conn, opts, headers) {

242
lib/tcp/index.js Normal file
View File

@ -0,0 +1,242 @@
'use strict';
module.exports.create = function (deps, config) {
console.log('config', config);
var listeners = require('../servers').listeners.tcp;
var domainUtils = require('../domain-utils');
var modules;
var addrProperties = [
'remoteAddress'
, 'remotePort'
, 'remoteFamily'
, 'localAddress'
, 'localPort'
, 'localFamily'
];
function nameMatchesDomains(name, domainList) {
return domainList.some(function (pattern) {
return domainUtils.match(pattern, name);
});
}
function proxy(mod, conn, opts) {
// First thing we need to add to the connection options is where to proxy the connection to
var newConnOpts = domainUtils.separatePort(mod.address || '');
newConnOpts.port = newConnOpts.port || mod.port;
newConnOpts.host = newConnOpts.host || mod.host || 'localhost';
// Then we add all of the connection address information. We need to prefix all of the
// properties with '_' so we can provide the information to any connection `createConnection`
// implementation but not have the default implementation try to bind the same local port.
addrProperties.forEach(function (name) {
newConnOpts['_' + name] = opts[name] || opts['_'+name] || conn[name] || conn['_'+name];
});
modules.proxy(conn, newConnOpts);
return true;
}
function checkTcpProxy(conn, opts) {
var proxied = false;
// TCP Proxying (ie routing based on domain name [vs local port]) only works for
// TLS wrapped connections, so if the opts don't give us a servername or don't tell us
// this is the decrypted side of a TLS connection we can't handle it here.
if (!opts.servername || !opts.encrypted) { return proxied; }
proxied = config.domains.some(function (dom) {
if (!dom.modules || !Array.isArray(dom.modules.tcp)) { return false; }
if (!nameMatchesDomains(opts.servername, dom.names)) { return false; }
return dom.modules.tcp.some(function (mod) {
if (mod.type !== 'proxy') { return false; }
return proxy(mod, conn, opts);
});
});
proxied = proxied || config.tcp.modules.some(function (mod) {
if (mod.type !== 'proxy') { return false; }
if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; }
return proxy(mod, conn, opts);
});
return proxied;
}
function checkTcpForward(conn, opts) {
// TCP forwarding (ie routing connections based on local port) requires the local port
if (!conn.localPort) { return false; }
return config.tcp.modules.some(function (mod) {
if (mod.type !== 'forward') { return false; }
if (mod.ports.indexOf(conn.localPort) < 0) { return false; }
return proxy(mod, conn, opts);
});
}
// opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
function peek(conn, firstChunk, opts) {
opts.firstChunk = firstChunk;
conn.__opts = opts;
// TODO port/service-based routing can do here
// TLS byte 1 is handshake and byte 6 is client hello
if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) {
modules.tls.emit('connection', conn);
return;
}
// This doesn't work with TLS, but now that we know this isn't a TLS connection we can
// unshift the first chunk back onto the connection for future use. The unshift should
// happen after any listeners are attached to it but before any new data comes in.
if (!opts.hyperPeek) {
process.nextTick(function () {
conn.unshift(firstChunk);
});
}
// Connection is not TLS, check for HTTP next.
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
var firstStr = firstChunk.toString();
if (/HTTP\//i.test(firstStr)) {
modules.http.emit('connection', conn);
return;
}
}
console.warn('failed to identify protocol from first chunk', firstChunk);
conn.destroy();
}
function tcpHandler(conn, opts) {
function getProp(name) {
return opts[name] || opts['_'+name] || conn[name] || conn['_'+name];
}
opts = opts || {};
var logName = getProp('remoteAddress') + ':' + getProp('remotePort') + ' -> ' +
getProp('localAddress') + ':' + getProp('localPort');
console.log('[tcpHandler]', logName, 'connection started - encrypted: ' + (opts.encrypted || false));
var start = Date.now();
conn.on('timeout', function () {
console.log('[tcpHandler]', logName, 'connection timed out', (Date.now()-start)/1000);
});
conn.on('end', function () {
console.log('[tcpHandler]', logName, 'connection ended', (Date.now()-start)/1000);
});
conn.on('close', function () {
console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000);
});
if (checkTcpForward(conn, opts)) { return; }
if (checkTcpProxy(conn, opts)) { return; }
// XXX PEEK COMMENT XXX
// TODO we can have our cake and eat it too
// we can skip the need to wrap the TLS connection twice
// because we've already peeked at the data,
// but this needs to be handled better before we enable that
// (because it creates new edge cases)
if (opts.hyperPeek) {
console.log('hyperpeek');
peek(conn, opts.firstChunk, opts);
return;
}
function onError(err) {
console.error('[error] socket errored peeking -', err);
conn.destroy();
}
conn.once('error', onError);
conn.once('data', function (chunk) {
conn.removeListener('error', onError);
peek(conn, chunk, opts);
});
}
process.nextTick(function () {
modules = {};
modules.tcpHandler = tcpHandler;
modules.proxy = require('./proxy-conn').create(deps, config);
modules.tls = require('./tls').create(deps, config, modules);
modules.http = require('./http').create(deps, config, modules);
});
function updateListeners() {
var current = listeners.list();
var wanted = config.tcp.bind;
if (!Array.isArray(wanted)) { wanted = []; }
wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356);
var closeProms = current.filter(function (port) {
return wanted.indexOf(port) < 0;
}).map(function (port) {
return listeners.close(port, 1000);
});
// We don't really need to filter here since listening on the same port with the
// same handler function twice is basically a no-op.
var openProms = wanted.map(function (port) {
return listeners.add(port, tcpHandler);
});
return Promise.all(closeProms.concat(openProms));
}
var mainPort;
function updateConf() {
updateListeners().catch(function (err) {
console.error('Error updating TCP listeners to match bind configuration');
console.error(err);
});
var unforwarded = {};
config.tcp.bind.forEach(function (port) {
unforwarded[port] = true;
});
config.tcp.modules.forEach(function (mod) {
if (['forward', 'proxy'].indexOf(mod.type) < 0) {
console.warn('unknown TCP module type specified', JSON.stringify(mod));
}
if (mod.type !== 'forward') { return; }
mod.ports.forEach(function (port) {
if (!unforwarded[port]) {
console.warn('trying to forward TCP port ' + port + ' multiple times or it is unbound');
} else {
delete unforwarded[port];
}
});
});
// Not really sure what we can reasonably do to prevent this. At least not without making
// our configuration validation more complicated.
if (!Object.keys(unforwarded).length) {
console.warn('no bound TCP ports are not being forwarded, admin interface will be inaccessible');
}
// If we are listening on port 443 make that the main port we respond to mDNS queries with
// otherwise choose the lowest number port we are bound to but not forwarding.
if (unforwarded['443']) {
mainPort = 443;
} else {
mainPort = Object.keys(unforwarded).map(Number).sort((a, b) => a - b)[0];
}
}
updateConf();
var result = {
updateConf
, handler: tcpHandler
};
Object.defineProperty(result, 'mainPort', {enumerable: true, get: () => mainPort});
return result;
};

View File

@ -32,7 +32,7 @@ module.exports.getRespBody = getRespBody;
module.exports.sendBadGateway = sendBadGateway; module.exports.sendBadGateway = sendBadGateway;
module.exports.create = function (deps, config) { module.exports.create = function (deps, config) {
return function proxy(conn, newConnOpts, firstChunk, decrypt) { function proxy(conn, newConnOpts, firstChunk, decrypt) {
var connected = false; var connected = false;
newConnOpts.allowHalfOpen = true; newConnOpts.allowHalfOpen = true;
var newConn = deps.net.createConnection(newConnOpts, function () { var newConn = deps.net.createConnection(newConnOpts, function () {
@ -73,5 +73,9 @@ module.exports.create = function (deps, config) {
newConn.on('close', function () { newConn.on('close', function () {
conn.destroy(); conn.destroy();
}); });
}; }
proxy.getRespBody = getRespBody;
proxy.sendBadGateway = sendBadGateway;
return proxy;
}; };

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
module.exports.create = function (deps, config, netHandler) { module.exports.create = function (deps, config, tcpMods) {
var path = require('path'); var path = require('path');
var tls = require('tls'); var tls = require('tls');
var parseSni = require('sni'); var parseSni = require('sni');
@ -86,8 +86,7 @@ module.exports.create = function (deps, config, netHandler) {
, challenges: { , challenges: {
'http-01': require('le-challenge-fs').create({ debug: config.debug }) 'http-01': require('le-challenge-fs').create({ debug: config.debug })
, 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug }) , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug })
// TODO dns-01 , 'dns-01': deps.ddns.challenge
//, 'dns-01': require('le-challenge-ddns').create({ debug: config.debug })
} }
, challengeType: 'http-01' , challengeType: 'http-01'
@ -208,7 +207,7 @@ module.exports.create = function (deps, config, netHandler) {
var terminateServer = tls.createServer(terminatorOpts, function (socket) { var terminateServer = tls.createServer(terminatorOpts, function (socket) {
console.log('(post-terminated) tls connection, addr:', extractSocketProp(socket, 'remoteAddress')); console.log('(post-terminated) tls connection, addr:', extractSocketProp(socket, 'remoteAddress'));
netHandler(socket, { tcpMods.tcpHandler(socket, {
servername: socket.servername servername: socket.servername
, encrypted: true , encrypted: true
// remoteAddress... ugh... https://github.com/nodejs/node/issues/8854 // remoteAddress... ugh... https://github.com/nodejs/node/issues/8854
@ -232,7 +231,7 @@ module.exports.create = function (deps, config, netHandler) {
newConnOpts.remoteAddress = opts.address || extractSocketProp(socket, 'remoteAddress'); newConnOpts.remoteAddress = opts.address || extractSocketProp(socket, 'remoteAddress');
newConnOpts.remotePort = opts.port || extractSocketProp(socket, 'remotePort'); newConnOpts.remotePort = opts.port || extractSocketProp(socket, 'remotePort');
deps.proxy(socket, newConnOpts, opts.firstChunk, function () { tcpMods.proxy(socket, newConnOpts, opts.firstChunk, function () {
// This function is called in the event of a connection error and should decrypt // This function is called in the event of a connection error and should decrypt
// the socket so the proxy module can send a 502 HTTP response. // the socket so the proxy module can send a 502 HTTP response.
var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true}); var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true});
@ -291,8 +290,8 @@ module.exports.create = function (deps, config, netHandler) {
return; return;
} }
if (deps.tunnelServer.isClientDomain(opts.servername)) { if (deps.stunneld.isClientDomain(opts.servername)) {
deps.tunnelServer.handleClientConn(socket); deps.stunneld.handleClientConn(socket);
if (!opts.hyperPeek) { if (!opts.hyperPeek) {
process.nextTick(function () { process.nextTick(function () {
socket.unshift(opts.firstChunk); socket.unshift(opts.firstChunk);

View File

@ -1,26 +1,10 @@
'use strict'; 'use strict';
module.exports.create = function (deps, config) { function httpsTunnel(servername, conn) {
if (!config.tunnelServer || !Array.isArray(config.tunnelServer.servernames) || !config.tunnelServer.secret) {
return {
isAdminDomain: function () { return false; }
, isClientDomain: function () { return false; }
};
}
var tunnelOpts = Object.assign({}, config.tunnelServer);
// This function should not be called because connections to the admin domains
// should already be decrypted, and connections to non-client domains should never
// be given to us in the first place.
tunnelOpts.httpsTunnel = function (servername, conn) {
console.error('tunnel server received encrypted connection to', servername); console.error('tunnel server received encrypted connection to', servername);
conn.end(); conn.end();
}; }
tunnelOpts.httpsInvalid = tunnelOpts.httpsTunnel; function handleHttp(servername, conn) {
// This function should not be called because ACME challenges should be handled
// before admin domain connections are given to us, and the only non-encrypted
// client connections that should be given to us are ACME challenges.
tunnelOpts.handleHttp = function (servername, conn) {
console.error('tunnel server received un-encrypted connection to', servername); console.error('tunnel server received un-encrypted connection to', servername);
conn.end([ conn.end([
'HTTP/1.1 404 Not Found' 'HTTP/1.1 404 Not Found'
@ -31,31 +15,117 @@ module.exports.create = function (deps, config) {
, '' , ''
, 'Not Found' , 'Not Found'
].join('\r\n')); ].join('\r\n'));
}; }
tunnelOpts.handleInsecureHttp = tunnelOpts.handleHttp; function rejectNonWebsocket(req, res) {
var tunnelServer = require('stunneld').create(tunnelOpts);
var httpServer = require('http').createServer(function (req, res) {
// status code 426 = Upgrade Required // status code 426 = Upgrade Required
res.statusCode = 426; res.statusCode = 426;
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({error: { res.send({error: { message: 'Only websockets accepted for tunnel server' }});
message: 'Only websockets accepted for tunnel server' }
}}));
}); var defaultConfig = {
var wsServer = new (require('ws').Server)({ server: httpServer }); servernames: []
wsServer.on('connection', tunnelServer.ws); , secret: null
};
var tunnelFuncs = {
// These functions should not be called because connections to the admin domains
// should already be decrypted, and connections to non-client domains should never
// be given to us in the first place.
httpsTunnel: httpsTunnel
, httpsInvalid: httpsTunnel
// These function should not be called because ACME challenges should be handled
// before admin domain connections are given to us, and the only non-encrypted
// client connections that should be given to us are ACME challenges.
, handleHttp: handleHttp
, handleInsecureHttp: handleHttp
};
module.exports.create = function (deps, config) {
var equal = require('deep-equal');
var enableDestroy = require('server-destroy');
var currentOpts = Object.assign({}, defaultConfig);
var httpServer, wsServer, stunneld;
function start() {
if (httpServer || wsServer || stunneld) {
throw new Error('trying to start already started tunnel server');
}
httpServer = require('http').createServer(rejectNonWebsocket);
enableDestroy(httpServer);
wsServer = new (require('ws').Server)({ server: httpServer });
var tunnelOpts = Object.assign({}, tunnelFuncs, currentOpts);
stunneld = require('stunneld').create(tunnelOpts);
wsServer.on('connection', stunneld.ws);
}
function stop() {
if (!httpServer || !wsServer || !stunneld) {
throw new Error('trying to stop unstarted tunnel server (or it got into semi-initialized state');
}
wsServer.close();
wsServer = null;
httpServer.destroy();
httpServer = null;
// Nothing to close here, just need to set it to null to allow it to be garbage-collected.
stunneld = null;
}
function updateConf() {
var newOpts = Object.assign({}, defaultConfig, config.tunnelServer);
if (!Array.isArray(newOpts.servernames)) {
newOpts.servernames = [];
}
var trimmedOpts = {
servernames: newOpts.servernames.slice().sort()
, secret: newOpts.secret
};
if (equal(trimmedOpts, currentOpts)) {
return;
}
currentOpts = trimmedOpts;
// Stop what's currently running, then if we are still supposed to be running then we
// can start it again with the updated options. It might be possible to make use of
// the existing http and ws servers when the config changes, but I'm not sure what
// state the actions needed to close all existing connections would put them in.
if (httpServer || wsServer || stunneld) {
stop();
}
if (currentOpts.servernames.length && currentOpts.secret) {
start();
}
}
process.nextTick(updateConf);
return { return {
isAdminDomain: function (domain) { isAdminDomain: function (domain) {
return config.tunnelServer.servernames.indexOf(domain) !== -1; return currentOpts.servernames.indexOf(domain) !== -1;
} }
, handleAdminConn: function (conn) { , handleAdminConn: function (conn) {
httpServer.emit('connection', conn); if (!httpServer) {
console.error(new Error('handleAdminConn called with no active tunnel server'));
conn.end();
} else {
return httpServer.emit('connection', conn);
}
} }
, isClientDomain: tunnelServer.isClientDomain , isClientDomain: function (domain) {
, handleClientConn: tunnelServer.tcp if (!stunneld) { return false; }
return stunneld.isClientDomain(domain);
}
, handleClientConn: function (conn) {
if (!stunneld) {
console.error(new Error('handleClientConn called with no active tunnel server'));
conn.end();
} else {
return stunneld.tcp(conn);
}
}
, updateConf
}; };
}; };

57
lib/udp.js Normal file
View File

@ -0,0 +1,57 @@
'use strict';
module.exports.create = function (deps, config) {
var listeners = require('./servers').listeners.udp;
function packetHandler(port, msg) {
if (!Array.isArray(config.udp.modules)) {
return;
}
var socket = require('dgram').createSocket('udp4');
config.udp.modules.forEach(function (mod) {
if (mod.type !== 'forward') {
// To avoid logging bad modules every time we get a UDP packet we assign a warned
// property to the module (non-enumerable so it won't be saved to the config or
// show up in the API).
if (!mod.warned) {
console.warn('found bad DNS module', mod);
Object.defineProperty(mod, 'warned', {value: true, enumerable: false});
}
return;
}
if (mod.ports.indexOf(port) < 0) {
return;
}
var dest = require('./domain-utils').separatePort(mod.address || '');
dest.port = dest.port || mod.port;
dest.host = dest.host || mod.host || 'localhost';
socket.send(msg, dest.port, dest.host);
});
}
function updateListeners() {
var current = listeners.list();
var wanted = config.udp.bind;
if (!Array.isArray(wanted)) { wanted = []; }
wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356);
current.forEach(function (port) {
if (wanted.indexOf(port) < 0) {
listeners.close(port);
}
});
wanted.forEach(function (port) {
if (current.indexOf(port) < 0) {
listeners.add(port, packetHandler.bind(port));
}
});
}
updateListeners();
return {
updateConf: updateListeners
};
};

View File

@ -48,13 +48,15 @@ function create(conf) {
modules = { modules = {
storage: require('./storage').create(deps, conf) storage: require('./storage').create(deps, conf)
, proxy: require('./proxy-conn').create(deps, conf)
, socks5: require('./socks5-server').create(deps, conf) , socks5: require('./socks5-server').create(deps, conf)
, ddns: require('./ddns').create(deps, conf) , ddns: require('./ddns').create(deps, conf)
, mdns: require('./mdns').create(deps, conf)
, udp: require('./udp').create(deps, conf)
, tcp: require('./tcp').create(deps, conf)
, stunneld: require('./tunnel-server-manager').create(deps, config)
}; };
Object.assign(deps, modules); Object.assign(deps, modules);
require('./goldilocks.js').create(deps, conf);
process.removeListener('message', create); process.removeListener('message', create);
process.on('message', update); process.on('message', update);
} }

2316
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
{ {
"name": "goldilocks", "name": "goldilocks",
"version": "1.1.3", "version": "1.1.6",
"description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.", "description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.",
"main": "bin/goldilocks.js", "main": "bin/goldilocks.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@git.daplie.com:Daplie/goldilocks.js.git" "url": "git.coolaj86.com:coolaj86/goldilocks.js.git"
}, },
"author": "AJ ONeal <aj@daplie.com> (https://daplie.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)", "license": "(MIT OR Apache-2.0)",
"scripts": { "scripts": {
"test": "node bin/goldilocks.js -p 8443 -d /tmp/" "test": "node bin/goldilocks.js -p 8443 -d /tmp/"
@ -34,42 +34,41 @@
"server" "server"
], ],
"bugs": { "bugs": {
"url": "https://git.daplie.com/Daplie/server-https/issues" "url": "https://git.coolaj86.com/coolaj86/goldilocks.js/issues"
}, },
"homepage": "https://git.daplie.com/Daplie/goldilocks.js", "homepage": "https://git.coolaj86.com/coolaj86/goldilocks.js",
"dependencies": { "dependencies": {
"bluebird": "^3.4.6", "bluebird": "^3.4.6",
"body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", "body-parser": "1",
"commander": "^2.9.0", "commander": "^2.9.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"dns-suite": "git+https://git@git.daplie.com/Daplie/dns-suite#v1", "dns-suite": "1",
"express": "git+https://github.com/expressjs/express.git#4.x", "express": "4",
"finalhandler": "^0.4.0", "finalhandler": "^0.4.0",
"greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", "greenlock": "2.1",
"http-proxy": "^1.16.2", "http-proxy": "^1.16.2",
"human-readable-ids": "git+https://git.daplie.com/Daplie/human-readable-ids-js#master", "human-readable-ids": "1",
"ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", "ipaddr.js": "v1.3",
"js-yaml": "^3.8.3", "js-yaml": "^3.8.3",
"jsonschema": "^1.2.0", "jsonschema": "^1.2.0",
"jsonwebtoken": "^7.4.0", "jsonwebtoken": "^7.4.0",
"le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", "le-challenge-fs": "2",
"le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master",
"le-challenge-sni": "^2.0.1", "le-challenge-sni": "^2.0.1",
"le-store-certbot": "git+https://git.daplie.com/Daplie/le-store-certbot.git#master", "le-store-certbot": "2",
"localhost.daplie.me-certificates": "^1.3.5", "localhost.daplie.me-certificates": "^1.3.5",
"network": "^0.4.0", "network": "^0.4.0",
"recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4", "recase": "v1.0.4",
"redirect-https": "^1.1.0", "redirect-https": "^1.1.0",
"request": "^2.81.0", "request": "^2.81.0",
"scmp": "git+https://github.com/freewil/scmp.git#1.x", "scmp": "1",
"serve-index": "^1.7.0", "serve-index": "^1.7.0",
"serve-static": "^1.10.0", "serve-static": "^1.10.0",
"server-destroy": "^1.0.1", "server-destroy": "^1.0.1",
"sni": "^1.0.0", "sni": "^1.0.0",
"socket-pair": "^1.0.3", "socket-pair": "^1.0.3",
"socksv5": "0.0.6", "socksv5": "0.0.6",
"stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1", "stunnel": "1.0",
"stunneld": "git+https://git.daplie.com/Daplie/node-tunnel-server.git#v1", "stunneld": "0.9",
"tunnel-packer": "^1.3.0", "tunnel-packer": "^1.3.0",
"ws": "^2.3.1" "ws": "^2.3.1"
} }

View File

@ -1,3 +0,0 @@
# adding TOS to TXT DNS Record
daplie dns:set -n _terms._cloud.localhost.foo.daplie.me -t TXT -a '{"url":"oauth3.org/tos/draft","explicit":true}' --ttl 3600
daplie dns:set -n _terms._cloud.localhost.alpha.daplie.me -t TXT -a '{"url":"oauth3.org/tos/draft","explicit":true}' --ttl 3600

View File

@ -1,17 +0,0 @@
#!/bin/bash
node serve.js \
--port 8443 \
--key node_modules/localhost.daplie.me-certificates/privkey.pem \
--cert node_modules/localhost.daplie.me-certificates/fullchain.pem \
--root node_modules/localhost.daplie.me-certificates/root.pem \
-c "$(cat node_modules/localhost.daplie.me-certificates/root.pem)" &
PID=$!
sleep 1
curl -s --insecure http://localhost.daplie.me:8443 > ./root.pem
curl -s https://localhost.daplie.me:8443 --cacert ./root.pem
rm ./root.pem
kill $PID 2>/dev/null