Compare commits

..

83 Commits

Author SHA1 Message Date
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 11809030c6 use sudo_cmd as needed 2017-11-07 16:19:40 -07:00
AJ ONeal b6b9d5f2f3 Merge branch 'v1.1' 2017-11-07 16:12:13 -07:00
AJ ONeal b307a2bcf2 forcefully preserve / permissions 2017-11-07 16:12:05 -07:00
AJ ONeal 0a233cfcf0 Merge branch 'v1.1' 2017-11-07 16:08:34 -07:00
AJ ONeal 4ffad8d3c3 fix dirname expansion 2017-11-07 16:06:43 -07:00
AJ ONeal 0e1437bcd7 fix dirname expansion 2017-11-07 16:05:14 -07:00
AJ ONeal a17f7d52ba fix instructions 2017-11-07 16:03:27 -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 ce31c2c02d correct which files to remove 2017-11-07 15:58:57 -07:00
AJ ONeal 4baf475e35 adjust logs 2017-11-07 15:56:09 -07:00
AJ ONeal 0611645ef0 adjust tmpfiles.d 2017-11-07 15:54:59 -07:00
AJ ONeal 0024d51289 Merge branch 'v1' 2017-11-07 15:45:46 -07:00
AJ ONeal 62b4c79236 update Uninstall 2017-11-07 15:45:11 -07:00
AJ ONeal fbdf0e8a28 don't let perms on / get messed up by systemd 2017-11-07 15:39:36 -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 ccf45ab06e merge with v1.1 2017-11-07 15:06:29 -07:00
AJ ONeal ac36a35c19 Merge branch 'installer-v2' 2017-11-07 15:02:57 -07:00
AJ ONeal a2d81e4302 use home folder 2017-11-07 15:02:49 -07:00
AJ ONeal 6ae1e463c9 don't change existing files and folders 2017-11-07 14:59:31 -07:00
AJ ONeal 8ee24fcd77 curl | bash 2017-11-07 14:30:07 -07:00
AJ ONeal 8c34316979 Merge branch 'installer-v2' 2017-11-07 14:28:51 -07:00
AJ ONeal 011559b1a4 ignore tmpfiles.d 2017-11-07 14:28:30 -07:00
AJ ONeal 65920f8fce Merge branch 'installer-v2' 2017-11-07 21:02:02 +00:00
AJ ONeal 32f2f707cc keep my_root as root:root 2017-11-07 21:01:41 +00:00
AJ ONeal 75d2680830 Merge branch 'installer-v2' 2017-11-07 20:59:17 +00:00
AJ ONeal a2d1797d0f set root level dirs to root ownership 2017-11-07 20:58:58 +00:00
AJ ONeal 0b464cab36 Merge branch 'installer-v2' 2017-11-07 20:55:31 +00:00
AJ ONeal 07920b594c use correct name, duh 2017-11-07 20:55:12 +00:00
AJ ONeal 0935e3e4b3 change dir from which it runs 2017-11-07 20:54:15 +00:00
AJ ONeal 35016cd124 Merge branch 'master' of ssh://git.daplie.com/Daplie/goldilocks.js 2017-11-07 20:52:53 +00:00
Your Name cec4f1ee95 show how to install 2017-11-07 20:52:25 +00:00
AJ ONeal 4b2e6b1600 Merge branch 'master' of git.daplie.com:Daplie/goldilocks.js 2017-11-07 13:41:56 -07:00
AJ ONeal 352b1b0a4a support curl-bash and git clone 2017-11-07 13:41:10 -07:00
AJ ONeal c40a17dceb place our node path BEFORE theirs 2017-11-07 12:25:01 -07:00
AJ ONeal 186a68a0ad don't exit with bad status code 2017-11-07 12:16:19 -07:00
tigerbot e071b8c3eb v1.1.4 2017-11-07 10:32:34 -07:00
AJ ONeal fe477300aa show unistall instructions, etc 2017-11-07 05:42:55 -07:00
AJ ONeal 278ba38398 move my_app_name 2017-11-07 04:21:33 -07:00
AJ ONeal 041138f4b2 move my_tmp 2017-11-07 04:20:15 -07:00
AJ ONeal 3bb6dc9680 run from cloned folder 2017-11-07 04:09:01 -07:00
AJ ONeal 5c7a5c0b2e turn on set +e around if blocks 2017-11-06 18:30:41 -07:00
AJ ONeal 55f81ca1b6 update user in systemd script 2017-11-06 18:26:33 -07:00
AJ ONeal ecf5f038dd test without [ 2017-11-06 18:11:05 -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
AJ ONeal b332b1fc89 more exact ANDROID_ROOT 2017-11-06 18:01:40 -07:00
AJ ONeal 33c54149c0 fix symlinks and list directory to copy 2017-11-06 17:54:23 -07:00
AJ ONeal 669587a07e less verbose 2017-11-06 17:43:36 -07:00
AJ ONeal 64fc41377f WIP simpler installer 2017-11-06 17:37:30 -07:00
AJ ONeal 680cb05f89 WIP simpler installer 2017-11-06 17:35:55 -07:00
AJ ONeal 847824f97a WIP simpler installer 2017-11-06 17:34:35 -07:00
AJ ONeal 11715f1405 WIP simpler installer 2017-11-06 17:33:49 -07:00
AJ ONeal e0fe188846 WIP simpler installer 2017-11-06 17:30:14 -07:00
AJ ONeal 34ce5ed4ee WIP simpler installer 2017-11-06 17:00:25 -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
41 changed files with 1268 additions and 3182 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
* fixed (probably) network settings not being readable
* supports timeouts in loopback check

View File

@ -20,17 +20,51 @@ The node.js netserver that's just right.
Install Standalone
-------
### curl | bash
```bash
# v1 in npm
npm install -g goldilocks
curl -fsSL https://git.daplie.com/Daplie/goldilocks.js/raw/v1.1/installer/get.sh | bash
```
### git
```bash
git clone https://git.daplie.com/Daplie/goldilocks.js
pushd goldilocks.js
git checkout v1.1
bash installer/install.sh
```
### npm
```bash
# v1 in git (unauthenticated)
npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js#v1
# v1 in git (via ssh)
npm install -g git+ssh://git@git.daplie.com:Daplie/goldilocks.js#v1
# v1 in git (unauthenticated)
npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js#v1
# v1 in npm
npm install -g goldilocks@v1
```
### Uninstall
Remove goldilocks and services:
```
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
-----
```bash
goldilocks
```
@ -271,6 +305,12 @@ tls:
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
The tcp system handles both *raw* and *tls-terminated* tcp network traffic

View File

@ -19,14 +19,14 @@ StartLimitBurst=3
# User and group the process will run as
# (www-data is the de facto standard on most systems)
User=www-data
Group=www-data
User=MY_USER
Group=MY_GROUP
# If we need to pass environment variables in the future
Environment=GOLDILOCKS_PATH=/srv/www NODE_PATH=/opt/goldilocks/lib/node_modules NPM_CONFIG_PREFIX=/opt/goldilocks
# Set a sane working directory, sane flags, and specify how to reload the config file
WorkingDirectory=/srv/www
WorkingDirectory=/opt/goldilocks
ExecStart=/opt/goldilocks/bin/node /opt/goldilocks/bin/goldilocks --config /etc/goldilocks/goldilocks.yml
ExecReload=/bin/kill -USR1 $MAINPID
@ -46,7 +46,7 @@ ProtectSystem=full
# … except TLS/SSL, ACME, and Let's Encrypt certificates
# and /var/log/goldilocks, because we want a place where logs can go.
# This merely retains r/w access rights, it does not add any new. Must still be writable on the host!
ReadWriteDirectories=/etc/goldilocks /etc/ssl /srv/www /var/log/goldilocks
ReadWriteDirectories=/etc/goldilocks /etc/ssl /srv/www /var/log/goldilocks /opt/goldilocks
# you may also want to add other directories such as /opt/goldilocks /etc/acme /etc/letsencrypt
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories

5
dist/etc/tmpfiles.d/goldilocks.conf vendored Normal file
View File

@ -0,0 +1,5 @@
# /etc/tmpfiles.d/goldilocks.conf
# See https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html
# Type Path Mode UID GID Age Argument
d /run/goldilocks 0755 MY_USER MY_GROUP - -

View File

@ -1,10 +0,0 @@
# /etc/tmpfiles.d/goldilocks.conf
# See https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html
# Type Path Mode UID GID Age Argument
d /etc/goldilocks 0755 www-data www-data - -
d /opt/goldilocks 0775 www-data www-data - -
d /srv/www 0775 www-data www-data - -
d /etc/ssl/goldilocks 0750 www-data www-data - -
d /var/log/goldilocks 0750 www-data www-data - -
#d /run/goldilocks 0755 www-data www-data - -

View File

@ -1,224 +0,0 @@
#!/bin/bash
# something or other about android and tmux using PREFIX
#: "${PREFIX:=''}"
MY_ROOT=""
if [ -z "${PREFIX-}" ]; then
MY_ROOT=""
else
MY_ROOT="$PREFIX"
fi
# Not every platform has or needs sudo, gotta save them O(1)s...
sudo_cmd=""
((EUID)) && [[ -z "$ANDROID_ROOT" ]] && sudo_cmd="sudo"
###############################
# #
# http_get #
# boilerplate for curl / wget #
# #
###############################
# See https://git.daplie.com/Daplie/daplie-snippets/blob/master/bash/http-get.sh
http_get=""
http_opts=""
http_out=""
detect_http_get()
{
if type -p curl >/dev/null 2>&1; then
http_get="curl"
http_opts="-fsSL"
http_out="-o"
elif type -p wget >/dev/null 2>&1; then
http_get="wget"
http_opts="--quiet"
http_out="-O"
else
echo "Aborted, could not find curl or wget"
return 7
fi
}
dap_dl()
{
$http_get $http_opts $http_out "$2" "$1"
touch "$2"
}
dap_dl_bash()
{
dap_url=$1
#dap_args=$2
rm -rf dap-tmp-runner.sh
$http_get $http_opts $http_out dap-tmp-runner.sh "$dap_url"; bash dap-tmp-runner.sh; rm dap-tmp-runner.sh
}
detect_http_get
## END HTTP_GET ##
###################
# #
# Install service #
# #
###################
my_app_name=goldilocks
my_app_pkg_name=com.daplie.goldilocks.web
my_app_dir=$(mktemp -d)
installer_base="https://git.daplie.com/Daplie/goldilocks.js/raw/master"
my_app_etc_config="etc/${my_app_name}/${my_app_name}.yml"
my_app_etc_example_config="etc/${my_app_name}/${my_app_name}.example.yml"
my_app_systemd_service="etc/systemd/system/${my_app_name}.service"
my_app_systemd_tmpfiles="etc/tmpfiles.d/${my_app_name}.conf"
my_app_launchd_service="Library/LaunchDaemons/${my_app_pkg_name}.plist"
install_for_systemd()
{
echo ""
echo "Installing as systemd service"
echo ""
mkdir -p $(dirname "$my_app_dir/$my_app_systemd_service")
dap_dl "$installer_base/$my_app_systemd_service" "$my_app_dir/$my_app_systemd_service"
$sudo_cmd mv "$my_app_dir/$my_app_systemd_service" "$MY_ROOT/$my_app_systemd_service"
$sudo_cmd chown -R root:root "$MY_ROOT/$my_app_systemd_service"
$sudo_cmd chmod 644 "$MY_ROOT/$my_app_systemd_service"
mkdir -p $(dirname "$my_app_dir/$my_app_systemd_tmpfiles")
dap_dl "$installer_base/$my_app_systemd_tmpfiles" "$my_app_dir/$my_app_systemd_tmpfiles"
$sudo_cmd mv "$my_app_dir/$my_app_systemd_tmpfiles" "$MY_ROOT/$my_app_systemd_tmpfiles"
$sudo_cmd chown -R root:root "$MY_ROOT/$my_app_systemd_tmpfiles"
$sudo_cmd chmod 644 "$MY_ROOT/$my_app_systemd_tmpfiles"
$sudo_cmd systemctl stop "${my_app_name}.service" >/dev/null 2>/dev/null
$sudo_cmd systemctl daemon-reload
$sudo_cmd systemctl start "${my_app_name}.service"
$sudo_cmd systemctl enable "${my_app_name}.service"
echo "$my_app_name started with systemctl, check its status like so"
echo " $sudo_cmd systemctl status $my_app_name"
echo " $sudo_cmd journalctl -xe -u goldilocks"
}
install_for_launchd()
{
echo ""
echo "Installing as launchd service"
echo ""
# See http://www.launchd.info/
mkdir -p $(dirname "$my_app_dir/$my_app_launchd_service")
dap_dl "$installer_base/$my_app_launchd_service" "$my_app_dir/$my_app_launchd_service"
$sudo_cmd mv "$my_app_dir/$my_app_launchd_service" "$MY_ROOT/$my_app_launchd_service"
$sudo_cmd chown root:wheel "$MY_ROOT/$my_app_launchd_service"
$sudo_cmd chmod 0644 "$MY_ROOT/$my_app_launchd_service"
$sudo_cmd launchctl unload -w "$MY_ROOT/$my_app_launchd_service" >/dev/null 2>/dev/null
$sudo_cmd launchctl load -w "$MY_ROOT/$my_app_launchd_service"
echo "$my_app_name started with launchd"
}
install_etc_config()
{
$sudo_cmd mkdir -p $(dirname "$MY_ROOT/$my_app_etc_example_config")
mkdir -p $(dirname "$my_app_dir/$my_app_etc_example_config")
dap_dl "$installer_base/$my_app_etc_example_config" "$my_app_dir/$my_app_etc_example_config"
$sudo_cmd mv "$my_app_dir/$my_app_etc_example_config" "$MY_ROOT/$my_app_etc_example_config"
if [ ! -e "$MY_ROOT/$my_app_etc_config" ]; then
$sudo_cmd mkdir -p $(dirname "$MY_ROOT/$my_app_etc_config")
mkdir -p $(dirname "$my_app_dir/$my_app_etc_config")
dap_dl "$installer_base/$my_app_etc_config" "$my_app_dir/$my_app_etc_config"
$sudo_cmd mv "$my_app_dir/$my_app_etc_config" "$MY_ROOT/$my_app_etc_config"
fi
# OS X
$sudo_cmd chown -R _www:_www $(dirname "$MY_ROOT/$my_app_etc_config") || true
# Linux
$sudo_cmd chown -R www-data:www-data $(dirname "$MY_ROOT/$my_app_etc_config") || true
$sudo_cmd chmod 775 $(dirname "$MY_ROOT/$my_app_etc_config")
$sudo_cmd chmod 664 "$MY_ROOT/$my_app_etc_config"
}
install_service()
{
install_etc_config
installable=""
if [ -d "$MY_ROOT/etc/systemd/system" ]; then
install_for_systemd
installable="true"
fi
if [ -d "/Library/LaunchDaemons" ]; then
install_for_launchd
installable="true"
fi
if [ -z "$installable" ]; then
echo ""
echo "Unknown system service init type. You must install as a system service manually."
echo '(please file a bug with the output of "uname -a")'
echo ""
fi
}
## END SERVICE_INSTALL ##
set -e
set -u
# Install
# TODO install to tmp location, then move to /opt
export NODE_PATH=/opt/goldilocks/lib/node_modules
export NPM_CONFIG_PREFIX=/opt/goldilocks
$sudo_cmd mkdir -p /etc/goldilocks
$sudo_cmd mkdir -p /var/log/goldilocks
$sudo_cmd mkdir -p /srv/www
$sudo_cmd mkdir -p /var/www
$sudo_cmd mkdir -p /opt/goldilocks/{lib,bin,etc}
# Dependencies
dap_dl_bash "https://git.daplie.com/Daplie/node-install-script/raw/master/setup-min.sh"
# Change to user perms
# OS X or Linux
$sudo_cmd chown -R $(whoami) /opt/goldilocks/ || true
my_npm="$NPM_CONFIG_PREFIX/bin/npm"
$my_npm install -g npm@4
$my_npm install -g 'git+https://git@git.daplie.com/Daplie/goldilocks.js.git'
# Finish up with submodule
pushd /opt/goldilocks/lib/node_modules/goldilocks
bash ./update-packages.sh
popd
# Change to admin perms
# OS X
$sudo_cmd chown -R _www:_www /var/www /srv/www /opt/goldilocks || true
# Linux
$sudo_cmd chown -R www-data:www-data /var/www /srv/www /opt/goldilocks || true
# make sure the files are all read/write for the owner and group, and then set
# the setuid and setgid bits so that any files/directories created inside these
# directories have the same owner and group.
$sudo_cmd chmod -R ug+rwX /opt/goldilocks
find /opt/goldilocks -type d -exec $sudo_cmd chmod ug+s {} \;
# Uninstall
dap_dl "https://git.daplie.com/Daplie/goldilocks.js/raw/master/uninstall.sh" "./goldilocks-uninstall"
$sudo_cmd chmod 755 "./goldilocks-uninstall"
# OS X
$sudo_cmd chown root:wheel "./goldilocks-uninstall" || true
# Linux
$sudo_cmd chown root:root "./goldilocks-uninstall" || true
$sudo_cmd mv "./goldilocks-uninstall" "/usr/local/bin/uninstall-goldilocks"
# Install Service
install_service

20
installer/get.sh Normal file
View File

@ -0,0 +1,20 @@
set -e
set -u
my_name=goldilocks
# TODO provide an option to supply my_ver and my_tmp
my_ver=master
my_tmp=$(mktemp -d)
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
echo "Installing to $my_tmp (will be moved after install)"
pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name
git checkout $my_ver
source ./installer/install.sh
popd
echo "Installation successful, now cleaning up $my_tmp ..."
rm -rf $my_tmp
echo "Done"

48
installer/http-get.sh Normal file
View File

@ -0,0 +1,48 @@
###############################
# #
# http_get #
# boilerplate for curl / wget #
# #
###############################
# See https://git.daplie.com/Daplie/daplie-snippets/blob/master/bash/http-get.sh
_h_http_get=""
_h_http_opts=""
_h_http_out=""
detect_http_get()
{
set +e
if type -p curl >/dev/null 2>&1; then
_h_http_get="curl"
_h_http_opts="-fsSL"
_h_http_out="-o"
elif type -p wget >/dev/null 2>&1; then
_h_http_get="wget"
_h_http_opts="--quiet"
_h_http_out="-O"
else
echo "Aborted, could not find curl or wget"
return 7
fi
set -e
}
http_get()
{
$_h_http_get $_h_http_opts $_h_http_out "$2" "$1"
touch "$2"
}
http_bash()
{
_http_url=$1
#dap_args=$2
rm -rf dap-tmp-runner.sh
$_h_http_get $_h_http_opts $_h_http_out dap-tmp-runner.sh "$_http_url"; bash dap-tmp-runner.sh; rm dap-tmp-runner.sh
}
detect_http_get
## END HTTP_GET ##

View File

@ -0,0 +1,17 @@
set -u
my_app_launchd_service="Library/LaunchDaemons/${my_app_pkg_name}.plist"
echo ""
echo "Installing as launchd service"
echo ""
# See http://www.launchd.info/
safe_copy_config "$my_app_dist/$my_app_launchd_service" "$my_root/$my_app_launchd_service"
$sudo_cmd chown root:wheel "$my_root/$my_app_launchd_service"
$sudo_cmd launchctl unload -w "$my_root/$my_app_launchd_service" >/dev/null 2>/dev/null
$sudo_cmd launchctl load -w "$my_root/$my_app_launchd_service"
echo "$my_app_name started with launchd"

View File

@ -0,0 +1,35 @@
set -u
my_app_systemd_service="etc/systemd/system/${my_app_name}.service"
my_app_systemd_tmpfiles="etc/tmpfiles.d/${my_app_name}.conf"
echo ""
echo "Installing as systemd service"
echo ""
sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_service" > "$my_app_dist/$my_app_systemd_service.2"
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"
safe_copy_config "$my_app_dist/$my_app_systemd_service" "$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_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"
safe_copy_config "$my_app_dist/$my_app_systemd_tmpfiles" "$my_root/$my_app_systemd_tmpfiles"
$sudo_cmd systemctl stop "${my_app_name}.service" >/dev/null 2>/dev/null || true
$sudo_cmd systemctl daemon-reload
$sudo_cmd systemctl start "${my_app_name}.service"
$sudo_cmd systemctl enable "${my_app_name}.service"
echo ""
echo ""
echo "Fun systemd commands to remember:"
echo " $sudo_cmd systemctl daemon-reload"
echo " $sudo_cmd systemctl restart $my_app_name.service"
echo ""
echo "$my_app_name started with systemctl, check its status like so:"
echo " $sudo_cmd systemctl status $my_app_name"
echo " $sudo_cmd journalctl -xefu $my_app_name"
echo ""
echo ""

View File

@ -0,0 +1,37 @@
safe_copy_config()
{
src=$1
dst=$2
$sudo_cmd mkdir -p $(dirname "$dst")
if [ -f "$dst" ]; then
$sudo_cmd rsync -a "$src" "$dst.latest"
# TODO edit config file with $my_user and $my_group
if [ "$(cat $dst)" == "$(cat $dst.latest)" ]; then
$sudo_cmd rm $dst.latest
else
echo "MANUAL INTERVENTION REQUIRED: check the systemd script update and manually decide what you want to do"
echo "diff $dst $dst.latest"
$sudo_cmd chown -R root:root "$dst.latest"
fi
else
$sudo_cmd rsync -a --ignore-existing "$src" "$dst"
fi
$sudo_cmd chown -R root:root "$dst"
$sudo_cmd chmod 644 "$dst"
}
installable=""
if [ -d "$my_root/etc/systemd/system" ]; then
source ./installer/install-for-systemd.sh
installable="true"
fi
if [ -d "/Library/LaunchDaemons" ]; then
source ./installer/install-for-launchd.sh
installable="true"
fi
if [ -z "$installable" ]; then
echo ""
echo "Unknown system service init type. You must install as a system service manually."
echo '(please file a bug with the output of "uname -a")'
echo ""
fi

149
installer/install.sh Normal file
View File

@ -0,0 +1,149 @@
#!/bin/bash
set -e
set -u
### IMPORTANT ###
### VERSION ###
my_name=goldilocks
my_app_pkg_name=com.daplie.goldilocks.web
my_app_ver="v1.1"
my_azp_oauth3_ver="v1.2.3"
export NODE_VERSION="v8.9.0"
if [ -z "${my_tmp-}" ]; then
my_tmp="$(mktemp -d)"
mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name
echo "Installing to $my_tmp (will be moved after install)"
git clone ./ $my_tmp/opt/$my_name/lib/node_modules/$my_name
pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name
fi
#################
export NODE_PATH=$my_tmp/opt/$my_name/lib/node_modules
export PATH=$my_tmp/opt/$my_name/bin/:$PATH
export NPM_CONFIG_PREFIX=$my_tmp/opt/$my_name
my_npm="$NPM_CONFIG_PREFIX/bin/npm"
#################
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"
# Backwards compat
# some scripts still use the old names
my_app_dir=$my_tmp
my_app_name=$my_name
git checkout $my_app_ver
mkdir -p "$my_tmp/opt/$my_name"/{lib,bin,etc}
ln -s ../lib/node_modules/$my_name/bin/$my_name.js $my_tmp/opt/$my_name/bin/$my_name
ln -s ../lib/node_modules/$my_name/bin/$my_name.js $my_tmp/opt/$my_name/bin/$my_name.js
mkdir -p "$my_tmp/etc/$my_name"
chmod 775 "$my_tmp/etc/$my_name"
cat "$my_app_dist/etc/$my_name/$my_name.example.yml" > "$my_tmp/etc/$my_name/$my_name.example.yml"
chmod 664 "$my_tmp/etc/$my_name/$my_name.example.yml"
mkdir -p $my_tmp/srv/www
mkdir -p $my_tmp/var/www
mkdir -p $my_tmp/var/log/$my_name
#
# Helpers
#
source ./installer/sudo-cmd.sh
source ./installer/http-get.sh
#
# Dependencies
#
echo $NODE_VERSION > /tmp/NODEJS_VER
http_bash "https://git.coolaj86.com/coolaj86/node-installer.sh/raw/v1.1/install.sh"
$my_npm install -g npm@4
pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name
$my_npm install
popd
pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name/packages/assets
OAUTH3_GIT_URL="https://git.oauth3.org/OAuth3/oauth3.js.git"
git clone ${OAUTH3_GIT_URL} oauth3.org || true
ln -s oauth3.org org.oauth3
pushd oauth3.org
git remote set-url origin ${OAUTH3_GIT_URL}
git checkout $my_azp_oauth3_ver
git pull
popd
mkdir -p jquery.com
ln -s jquery.com com.jquery
pushd jquery.com
http_get 'https://code.jquery.com/jquery-3.1.1.js' jquery-3.1.1.js
popd
mkdir -p google.com
ln -s google.com com.google
pushd google.com
http_get 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js' angular.1.6.2.min.js
popd
mkdir -p well-known
ln -s well-known .well-known
pushd well-known
ln -snf ../oauth3.org/well-known/oauth3 ./oauth3
popd
echo "installed dependencies"
popd
#
# System Service
#
source ./installer/my-root.sh
echo "Pre-installation to $my_tmp complete, now installing to $my_root/ ..."
set +e
if type -p tree >/dev/null 2>/dev/null; then
#tree -I "node_modules|include|share" $my_tmp
tree -L 6 -I "include|share|npm" $my_tmp
else
ls $my_tmp
fi
set -e
source ./installer/my-user-my-group.sh
echo "User $my_user Group $my_group"
$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 chmod 0755 $my_tmp
# don't change permissions on /, /etc, etc
$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
source ./installer/install-system-service.sh
# 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/var/www $my_root/srv/www
# make sure the files are all read/write for the owner and group, and then set
# the setuid and setgid bits so that any files/directories created inside these
# directories have the same owner and group.
$sudo_cmd chmod -R ug+rwX $my_root/opt/$my_name
find $my_root/opt/$my_name -type d -exec $sudo_cmd chmod ug+s {} \;
echo ""
echo "$my_name installation complete!"
echo ""
echo ""
echo "Update the config at: /etc/$my_name/$my_name.yml"
echo ""
echo "Unistall: rm -rf /srv/$my_name/ /var/$my_name/ /etc/$my_name/ /opt/$my_name/ /var/log/$my_name/ /etc/tmpfiles.d/$my_name.conf /etc/systemd/system/$my_name.service /etc/ssl/$my_name"

8
installer/my-root.sh Normal file
View File

@ -0,0 +1,8 @@
# something or other about android and tmux using PREFIX
#: "${PREFIX:=''}"
my_root=""
if [ -z "${PREFIX-}" ]; then
my_root=""
else
my_root="$PREFIX"
fi

View File

@ -0,0 +1,19 @@
if type -p adduser >/dev/null 2>/dev/null; then
if [ -z "$(cat $my_root/etc/passwd | grep $my_app_name)" ]; then
$sudo_cmd adduser --home $my_root/opt/$my_app_name --gecos '' --disabled-password $my_app_name
fi
my_user=$my_app_name
my_group=$my_app_name
elif [ -n "$(cat /etc/passwd | grep www-data:)" ]; then
# Linux (Ubuntu)
my_user=www-data
my_group=www-data
elif [ -n "$(cat /etc/passwd | grep _www:)" ]; then
# Mac
my_user=_www
my_group=_www
else
# Unsure
my_user=$(whoami)
my_group=$(id -g -n)
fi

7
installer/sudo-cmd.sh Normal file
View File

@ -0,0 +1,7 @@
# Not every platform has or needs sudo, gotta save them O(1)s...
sudo_cmd=""
set +e
if type -p sudo >/dev/null 2>/dev/null; then
((EUID)) && [[ -z "${ANDROID_ROOT-}" ]] && sudo_cmd="sudo"
fi
set -e

View File

@ -174,6 +174,14 @@ var mdnsSchema = {
}
};
var tunnelSvrSchema = {
type: 'object'
, properties: {
servernames: { type: 'array', items: { type: 'string' }}
, secret: { type: 'string' }
}
};
var ddnsSchema = {
type: 'object'
, properties: {
@ -223,6 +231,7 @@ var mainSchema = {
, ddns: ddnsSchema
, socks5: socks5Schema
, device: deviceSchema
, tunnel_server: tunnelSvrSchema
}
, 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';
module.exports.create = function (deps, conf) {
module.exports.create = function (deps, conf, utils) {
function dnsType(addr) {
if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) {
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) {
var directives = await deps.OAUTH3.discover(session.token.aud);
@ -111,7 +55,7 @@ module.exports.create = function (deps, conf) {
return goodAddrDomains.indexOf(domain) < 0;
});
var oldDns = await splitDomains(directives.api, badAddrDomains);
var oldDns = await utils.splitDomains(directives.api, badAddrDomains);
var common = {
api: 'devices.detach'
, session: session
@ -124,7 +68,7 @@ module.exports.create = function (deps, conf) {
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 = {
api: 'devices.attach'
, session: session
@ -169,7 +113,7 @@ module.exports.create = function (deps, conf) {
async function removeDomains(session, domains) {
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 = {
api: 'devices.detach'
, session: session

View File

@ -3,48 +3,21 @@
module.exports.create = function (deps, conf) {
var dns = deps.PromiseA.promisifyAll(require('dns'));
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 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;
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;
async function startTunnel(tunnelSession, mod, domainList) {
try {
var dnsSession = await getSession(mod.tokenId);
var tunnelDomain = await deps.tunnelClients.start(tunnelSession || dnsSession, domainList);
var dnsSession = await utils.getSession(mod.tokenId);
var tunnelDomain = await tunnelClients.start(tunnelSession || dnsSession, domainList);
var addrList;
try {
@ -59,7 +32,9 @@ module.exports.create = function (deps, conf) {
throw new Error('failed to lookup IP for tunnel domain "' + tunnelDomain + '"');
}
await dnsCtrl.setDeviceAddress(dnsSession, addrList[0], domainList);
if (!mod.disabled) {
await dnsCtrl.setDeviceAddress(dnsSession, addrList[0], domainList);
}
} catch (err) {
console.log('error starting tunnel for', domainList.join(', '));
console.log(err);
@ -73,7 +48,7 @@ module.exports.create = function (deps, conf) {
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; }
return startTunnel(tunnelSession, mod, domainList);
@ -82,14 +57,14 @@ module.exports.create = function (deps, conf) {
tunnelActive = true;
}
async function disconnectTunnels() {
deps.tunnelClients.disconnect();
tunnelClients.disconnect();
tunnelActive = false;
await Promise.resolve();
}
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; }
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; }
@ -187,10 +162,10 @@ module.exports.create = function (deps, conf) {
}
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; }
return getSession(mod.tokenId).then(function (session) {
return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.setDeviceAddress(session, addr, domainList);
}).catch(function (err) {
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
// 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; }
prevMods[mod.id] = { mod, domainList };
return true;
}, prevConf);
iterateAllModules(function (mod, domainList) {
utils.iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return; }
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.
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;
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();
} else {
oldDomains = domainList.filter(function (domain) {
@ -249,7 +227,7 @@ module.exports.create = function (deps, conf) {
return;
}
return getSession(mod.tokenId).then(function (session) {
return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.removeDomains(session, oldDomains);
});
}).filter(Boolean));
@ -259,6 +237,9 @@ module.exports.create = function (deps, conf) {
// And add DNS records for any newly added domains.
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;
if (!prevMods[mod.id] || mod.tokenId !== prevMods[mod.id].mod.tokenId) {
newDomains = domainList.slice();
@ -274,7 +255,7 @@ module.exports.create = function (deps, conf) {
return;
}
return getSession(mod.tokenId).then(function (session) {
return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.setDeviceAddress(session, publicAddress, newDomains);
});
}).filter(Boolean));
@ -340,5 +321,6 @@ module.exports.create = function (deps, conf) {
, getDeviceAddresses: dnsCtrl.getDeviceAddresses
, recheckPubAddr: recheckPubAddr
, updateConf: updateConf
, challenge
};
};

View File

@ -6,6 +6,52 @@ module.exports.create = function (deps, config) {
var activeTunnels = {};
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) {
if (typeof data === 'string') {
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.
activeTunnels[data.tunnelUrl] = stunnel.connect({
stunneld: data.tunnelUrl
, net: deps.tunnel.net
, net: customNet
// 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
// 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 queryName = '_cloud._tcp.local';
var dnsSuite = require('dns-suite');
function createResponse(name, ownerIds, packet, ttl, mainPort) {
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) {
var socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true });
var dns = require('dns-suite');
module.exports.create = function (deps, config) {
var socket;
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);
var packet;
try {
packet = dns.DNSPacket.parse(message);
packet = dnsSuite.DNSPacket.parse(message);
}
catch (er) {
// `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.
if (packet.header.qr !== 0) {
return;
}
if (packet.header.qr !== 0) { return; }
// Only respond if they were asking for cloud devices.
if (packet.question.length !== 1 || packet.question[0].name !== queryName) {
return;
}
if (packet.question[0].typeName !== 'PTR' || packet.question[0].className !== 'IN' ) {
return;
}
if (packet.question.length !== 1) { return; }
if (packet.question[0].name !== queryName) { return; }
if (packet.question[0].typeName !== 'PTR') { return; }
if (packet.question[0].className !== 'IN' ) { return; }
var proms = [
deps.storage.mdnsId.get()
@ -131,7 +127,7 @@ module.exports.start = function (deps, config, mainPort) {
];
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();
if (now > nextBroadcast) {
socket.send(resp, config.mdns.port, config.mdns.broadcast);
@ -140,18 +136,68 @@ module.exports.start = function (deps, config, mainPort) {
socket.send(resp, rinfo.port, rinfo.address);
}
});
});
}
socket.bind(config.mdns.port, function () {
var addr = this.address();
console.log('bound on UDP %s:%d for mDNS', addr.address, addr.port);
function start() {
socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true });
socket.on('message', handlePacket);
socket.setBroadcast(true);
socket.addMembership(config.mdns.broadcast);
// This is supposed to be a local device discovery mechanism, so we shouldn't
// need to hop through any gateways. This helps with security by making it
// much more difficult for someone to use us as part of a DDoS attack by
// spoofing the UDP address a request came from.
socket.setTTL(1);
});
return new Promise(function (resolve, reject) {
socket.once('error', reject);
socket.bind(config.mdns.port, function () {
var addr = this.address();
console.log('bound on UDP %s:%d for mDNS', addr.address, addr.port);
socket.setBroadcast(true);
socket.addMembership(config.mdns.broadcast);
// This is supposed to be a local device discovery mechanism, so we shouldn't
// need to hop through any gateways. This helps with security by making it
// much more difficult for someone to use us as part of a DDoS attack by
// spoofing the UDP address a request came from.
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._closing) {
module.exports.destroyTcpListener(port);
}
else if (handler !== stat.handler) {
// we'll replace the current listener
stat.server.destroy();
} else {
// 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
// if the handlers are the same). Until there is reason to do otherwise we are
// opting for the replacement.
stat.handler = handler;
resolve();
return;
}
else {
// this exact listener is already open
resolve();
return;
}
}
var enableDestroy = require('server-destroy');
@ -34,7 +30,7 @@ module.exports.addTcpListener = function (port, handler) {
stat = serversMap[port] = {
server: server
, handler: handler
, _closing: null
, _closing: false
};
// 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) {
var stat = serversMap[port];
if (!stat) {
resolve();
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();
});
};
@ -84,7 +90,9 @@ module.exports.destroyTcpListener = function (port) {
}
};
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) {
start(config.socks5.port).catch(function (err) {
console.error('failed to start Socks5 proxy', err);
});
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) {
console.error('failed to start Socks5 proxy', err);
});
configEnabled = true;
}
}
process.nextTick(updateConf);
return {
curState: curState
, start: start
, stop: stop
curState
, start
, stop
, updateConf
};
};

View File

@ -1,6 +1,6 @@
'use strict';
module.exports.create = function (deps, conf, greenlockMiddleware) {
module.exports.create = function (deps, conf, tcpMods) {
var PromiseA = require('bluebird');
var statAsync = PromiseA.promisify(require('fs').stat);
var domainMatches = require('../domain-utils').match;
@ -162,8 +162,8 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
return false;
}
if (deps.tunnelServer.isClientDomain(separatePort(headers.host).host)) {
deps.tunnelServer.handleClientConn(conn);
if (deps.stunneld.isClientDomain(separatePort(headers.host).host)) {
deps.stunneld.handleClientConn(conn);
process.nextTick(function () {
conn.unshift(opts.firstChunk);
conn.resume();
@ -172,7 +172,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
}
if (!acmeServer) {
acmeServer = require('http').createServer(greenlockMiddleware);
acmeServer = require('http').createServer(tcpMods.tls.middleware);
}
return emitConnection(acmeServer, conn, opts);
}
@ -214,8 +214,8 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
return emitConnection(adminServer, conn, opts);
}
if (deps.tunnelServer.isAdminDomain(host)) {
deps.tunnelServer.handleAdminConn(conn);
if (deps.stunneld.isAdminDomain(host)) {
deps.stunneld.handleAdminConn(conn);
process.nextTick(function () {
conn.unshift(opts.firstChunk);
conn.resume();
@ -241,7 +241,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
res.statusCode = 502;
res.setHeader('Connection', 'close');
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) {
@ -292,7 +292,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
newConnOpts.remoteAddress = opts.address || conn.remoteAddress;
newConnOpts.remotePort = opts.port || conn.remotePort;
deps.proxy(conn, newConnOpts, opts.firstChunk);
tcpMods.proxy(conn, newConnOpts, opts.firstChunk);
}
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.create = function (deps, config) {
return function proxy(conn, newConnOpts, firstChunk, decrypt) {
function proxy(conn, newConnOpts, firstChunk, decrypt) {
var connected = false;
newConnOpts.allowHalfOpen = true;
var newConn = deps.net.createConnection(newConnOpts, function () {
@ -73,5 +73,9 @@ module.exports.create = function (deps, config) {
newConn.on('close', function () {
conn.destroy();
});
};
}
proxy.getRespBody = getRespBody;
proxy.sendBadGateway = sendBadGateway;
return proxy;
};

View File

@ -1,6 +1,6 @@
'use strict';
module.exports.create = function (deps, config, netHandler) {
module.exports.create = function (deps, config, tcpMods) {
var path = require('path');
var tls = require('tls');
var parseSni = require('sni');
@ -86,8 +86,7 @@ module.exports.create = function (deps, config, netHandler) {
, challenges: {
'http-01': require('le-challenge-fs').create({ debug: config.debug })
, 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug })
// TODO dns-01
//, 'dns-01': require('le-challenge-ddns').create({ debug: config.debug })
, 'dns-01': deps.ddns.challenge
}
, challengeType: 'http-01'
@ -208,7 +207,7 @@ module.exports.create = function (deps, config, netHandler) {
var terminateServer = tls.createServer(terminatorOpts, function (socket) {
console.log('(post-terminated) tls connection, addr:', extractSocketProp(socket, 'remoteAddress'));
netHandler(socket, {
tcpMods.tcpHandler(socket, {
servername: socket.servername
, encrypted: true
// 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.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
// the socket so the proxy module can send a 502 HTTP response.
var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true});
@ -291,8 +290,8 @@ module.exports.create = function (deps, config, netHandler) {
return;
}
if (deps.tunnelServer.isClientDomain(opts.servername)) {
deps.tunnelServer.handleClientConn(socket);
if (deps.stunneld.isClientDomain(opts.servername)) {
deps.stunneld.handleClientConn(socket);
if (!opts.hyperPeek) {
process.nextTick(function () {
socket.unshift(opts.firstChunk);

View File

@ -1,61 +1,131 @@
'use strict';
module.exports.create = function (deps, config) {
if (!config.tunnelServer || !Array.isArray(config.tunnelServer.servernames) || !config.tunnelServer.secret) {
return {
isAdminDomain: function () { return false; }
, isClientDomain: function () { return false; }
};
}
function httpsTunnel(servername, conn) {
console.error('tunnel server received encrypted connection to', servername);
conn.end();
}
function handleHttp(servername, conn) {
console.error('tunnel server received un-encrypted connection to', servername);
conn.end([
'HTTP/1.1 404 Not Found'
, 'Date: ' + (new Date()).toUTCString()
, 'Connection: close'
, 'Content-Type: text/html'
, 'Content-Length: 9'
, ''
, 'Not Found'
].join('\r\n'));
}
function rejectNonWebsocket(req, res) {
// status code 426 = Upgrade Required
res.statusCode = 426;
res.setHeader('Content-Type', 'application/json');
res.send({error: { message: 'Only websockets accepted for tunnel server' }});
}
var tunnelOpts = Object.assign({}, config.tunnelServer);
// This function should not be called because connections to the admin domains
var defaultConfig = {
servernames: []
, 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.
tunnelOpts.httpsTunnel = function (servername, conn) {
console.error('tunnel server received encrypted connection to', servername);
conn.end();
};
tunnelOpts.httpsInvalid = tunnelOpts.httpsTunnel;
// This function should not be called because ACME challenges should be handled
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.
tunnelOpts.handleHttp = function (servername, conn) {
console.error('tunnel server received un-encrypted connection to', servername);
conn.end([
'HTTP/1.1 404 Not Found'
, 'Date: ' + (new Date()).toUTCString()
, 'Connection: close'
, 'Content-Type: text/html'
, 'Content-Length: 9'
, ''
, 'Not Found'
].join('\r\n'));
};
tunnelOpts.handleInsecureHttp = tunnelOpts.handleHttp;
, handleHttp: handleHttp
, handleInsecureHttp: handleHttp
};
var tunnelServer = require('stunneld').create(tunnelOpts);
module.exports.create = function (deps, config) {
var equal = require('deep-equal');
var enableDestroy = require('server-destroy');
var currentOpts = Object.assign({}, defaultConfig);
var httpServer = require('http').createServer(function (req, res) {
// status code 426 = Upgrade Required
res.statusCode = 426;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({error: {
message: 'Only websockets accepted for tunnel server'
}}));
});
var wsServer = new (require('ws').Server)({ server: httpServer });
wsServer.on('connection', tunnelServer.ws);
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 {
isAdminDomain: function (domain) {
return config.tunnelServer.servernames.indexOf(domain) !== -1;
return currentOpts.servernames.indexOf(domain) !== -1;
}
, 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
, handleClientConn: tunnelServer.tcp
, isClientDomain: function (domain) {
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 = {
storage: require('./storage').create(deps, conf)
, proxy: require('./proxy-conn').create(deps, conf)
, socks5: require('./socks5-server').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);
require('./goldilocks.js').create(deps, conf);
process.removeListener('message', create);
process.on('message', update);
}

2316
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "goldilocks",
"version": "1.1.3",
"version": "1.1.5",
"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",
"repository": {
@ -52,7 +52,6 @@
"js-yaml": "^3.8.3",
"jsonschema": "^1.2.0",
"jsonwebtoken": "^7.4.0",
"le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master",
"le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master",
"le-challenge-sni": "^2.0.1",
"le-store-certbot": "git+https://git.daplie.com/Daplie/le-store-certbot.git#master",

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

View File

@ -1,48 +0,0 @@
#!/bin/bash
# something or other about android and tmux using PREFIX
#: "${PREFIX:=''}"
MY_ROOT=""
if [ -z "${PREFIX-}" ]; then
MY_ROOT=""
else
MY_ROOT="$PREFIX"
fi
# Not every platform has or needs sudo, gotta save them O(1)s...
sudo_cmd=""
((EUID)) && [[ -z "$ANDROID_ROOT" ]] && sudo_cmd="sudo"
# you don't want any oopsies when an rm -rf is involved...
set -e
set -u
my_app_name=goldilocks
my_app_pkg_name=com.daplie.goldilocks.web
my_app_etc_config="etc/${my_app_name}/${my_app_name}.yml"
my_app_systemd_service="etc/systemd/system/${my_app_name}.service"
my_app_systemd_tmpfiles="etc/tmpfiles.d/${my_app_name}.conf"
my_app_launchd_service="Library/LaunchDaemons/${my_app_pkg_name}.plist"
my_app_upstart_service="etc/init.d/${my_app_name}.conf"
$sudo_cmd rm -f /usr/local/bin/$my_app_name
$sudo_cmd rm -f /usr/local/bin/uninstall-$my_app_name
$sudo_cmd rm -rf /usr/local/lib/node_modules/$my_app_name
$sudo_cmd rm -f "$MY_ROOT/$my_app_etc_config"
$sudo_cmd rmdir -p $(dirname "$MY_ROOT/$my_app_etc_config") 2>/dev/null || true
$sudo_cmd rm -f "$MY_ROOT/$my_app_systemd_service"
$sudo_cmd rm -f "$MY_ROOT/$my_app_systemd_tmpfiles"
$sudo_cmd rm -f "$MY_ROOT/$my_app_launchd_service"
$sudo_cmd rm -f "$MY_ROOT/$my_app_upstart_service"
$sudo_cmd rm -rf /opt/$my_app_name
$sudo_cmd rm -rf /var/log/$my_app_name
# TODO flag for --purge
#rm -rf /etc/goldilocks
# TODO trap uninstall function
echo "uninstall complete: $my_app_name"

View File

@ -1,31 +0,0 @@
#!/bin/bash
set -e
set -u
pushd $(dirname ${0})/packages/assets
OAUTH3_GIT_URL="https://git.daplie.com/Oauth3/oauth3.js.git"
git clone ${OAUTH3_GIT_URL} org.oauth3 || true
pushd org.oauth3
git remote set-url origin ${OAUTH3_GIT_URL}
git checkout master
git pull
popd
mkdir -p com.jquery
pushd com.jquery
curl -o jquery-3.1.1.js 'https://code.jquery.com/jquery-3.1.1.js'
popd
mkdir -p com.google
pushd com.google
curl -o angular.1.6.2.min.js 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js'
popd
mkdir -p well-known
pushd well-known
ln -snf ../org.oauth3/well-known/oauth3 ./oauth3
popd
popd