Compare commits

...

144 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
8fb910ddf9 Update 'installer/install.sh' 2018-04-10 04:44:23 +00:00
158892f88c rebrand 2018-04-10 04:42:47 +00:00
e462978154 install service before chown 2017-12-11 22:24:26 +00:00
3a7e4cd2ab don't pull on detached head 2017-12-11 22:14:04 +00:00
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
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
65920f8fce Merge branch 'installer-v2' 2017-11-07 21:02:02 +00:00
32f2f707cc keep my_root as root:root 2017-11-07 21:01:41 +00:00
75d2680830 Merge branch 'installer-v2' 2017-11-07 20:59:17 +00:00
a2d1797d0f set root level dirs to root ownership 2017-11-07 20:58:58 +00:00
0b464cab36 Merge branch 'installer-v2' 2017-11-07 20:55:31 +00:00
07920b594c use correct name, duh 2017-11-07 20:55:12 +00:00
0935e3e4b3 change dir from which it runs 2017-11-07 20:54:15 +00:00
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
AJ ONeal
e3c99636c5 add standard files 2017-11-06 11:08:33 -07:00
tigerbot
28f28c6eb9 made DDNS care less when checking the gateway fails 2017-11-03 15:16:30 -06:00
tigerbot
ef5dcb81f4 fixed bug determining for which domains to set new DNS records 2017-11-03 14:36:27 -06:00
tigerbot
b4e967f152 made the loopback check more robust 2017-11-03 14:31:28 -06:00
tigerbot
5de8edb33d fixed incorrect behavior when loopback or tunnel initially fails 2017-11-03 14:31:15 -06:00
AJ ONeal
b1d5ed3b14 Do not use leading underscores for SNI. 2017-11-01 14:50:29 -06: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
AJ ONeal
4a576da545 Update README.md 2017-10-30 11:24:29 -06:00
AJ ONeal
af14149a13 updated docs for tcp.proxy and ssh 2017-10-30 11:16:20 -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
tigerbot
b44ad7b17a added documentation for the new tcp.proxy module 2017-10-26 15:44:19 -06:00
tigerbot
138f59bea3 implemented proxying decrypted TLS streams in raw form 2017-10-26 14:39:51 -06:00
tigerbot
0ef845f2d5 added some documentation for the tokens API 2017-10-26 12:07:27 -06:00
tigerbot
e504c4b717 Merge branch 'ddns'
# Conflicts:
#	README.md
#	etc/goldilocks/goldilocks.example.yml
2017-10-25 18:35:07 -06:00
tigerbot
20cf66c67d added CORS header needed after recent change to OAuth3 library requests 2017-10-25 13:35:06 -06:00
tigerbot
72ff65e833 fix some misc problem found using browser to access API 2017-10-25 11:00:06 -06:00
tigerbot
00de23ded7 implemented setting DNS records after tunnel connect
currently done automatically by API we get the tunnel token from, but in the
near-ish future that will be changed
2017-10-20 18:02:55 -06:00
tigerbot
82f0b45c56 implemented cleanup/update of DNS records on config change 2017-10-20 15:38:10 -06:00
tigerbot
acf2fd7764 looking at active tunnel session on DDNS config update 2017-10-19 17:45:05 -06:00
tigerbot
c23f5ae25b moved the session cache to be longer lasting 2017-10-19 12:58:04 -06:00
tigerbot
019e4fa063 made connectTunnel wait for connections to actually start 2017-10-19 12:37:08 -06:00
tigerbot
3aed276faf switched to newer config structure for setting DNS records 2017-10-18 16:06:44 -06:00
tigerbot
b9fac21b05 switched to using new config format when connecting to tunnel 2017-10-18 15:37:35 -06:00
tigerbot
c55c034f11 started using of the ddns.loopback setting 2017-10-18 13:48:08 -06:00
tigerbot
6b2b91ba26 updated the documentation and validation for DDNS settings 2017-10-18 12:06:01 -06:00
tigerbot
cfaa8d4959 added interface to save user tokens 2017-10-17 18:36:36 -06:00
tigerbot
9c7aaa4f98 reduced some duplication in handling error responses 2017-10-17 16:16:57 -06:00
tigerbot
f2ce3e9fe1 Merge branch 'api-rewrite' into ddns
# Conflicts:
#	API.md
#	bin/goldilocks.js
#	etc/goldilocks/goldilocks.example.yml
#	lib/admin/apis.js
#	lib/app.js
#	lib/worker.js
2017-10-17 13:07:52 -06:00
tigerbot
754ace5cb4 removed arguments that populate a deprecated config 2017-10-17 12:56:25 -06:00
tigerbot
72520679d8 updated the documentation for the config API 2017-10-16 12:59:45 -06:00
tigerbot
e15d4f830e updated the example config 2017-10-13 12:39:31 -06:00
tigerbot
5e9e2662e0 updated the config documentation in the README 2017-10-12 18:57:17 -06:00
tigerbot
663fdba446 changed the valid UDP module from 'proxy' to 'forward'
forward is based on incoming port, while proxy is based on domains
	and we don't have any domain names for raw UDP or TCP
2017-10-12 14:35:19 -06:00
tigerbot
0406d0cd93 removed the acme property from the tls config 2017-10-12 11:57:43 -06:00
tigerbot
503da9efd0 implemented routes to edit and delete modules and domains 2017-10-11 17:13:33 -06:00
tigerbot
2a57a1e12c fixed a few misc errors that appeared in testing 2017-10-11 13:06:24 -06:00
tigerbot
79ef9694b7 updated API to reflect moved domains 2017-10-11 12:18:01 -06:00
tigerbot
61af4707ee moved domains up a level to allow multiple module groups with same domain names 2017-10-11 12:11:20 -06:00
tigerbot
ea55d3cc73 removed bind from the http and tls settings 2017-10-10 12:34:32 -06:00
tigerbot
8371170a14 renamed dns settings to udp 2017-10-10 11:32:18 -06:00
tigerbot
485a223f86 implemented better management of arrays in the config 2017-10-10 11:08:19 -06:00
tigerbot
5761ab9d62 added JSON Schema to validate the config 2017-10-06 17:50:16 -06:00
tigerbot
8f4a733391 changed module config property name 2017-10-05 18:11:58 -06:00
tigerbot
ded53cf45c reduced a few lines of code 2017-10-05 18:10:59 -06:00
tigerbot
0380a8087f automatically add id to modules and domains 2017-10-04 18:27:29 -06:00
tigerbot
d04b750f87 changed the default config 2017-10-04 18:26:27 -06:00
tigerbot
cc6b34dd46 made it possible to GET specific parts of the config 2017-10-04 14:42:19 -06:00
tigerbot
12e4a47855 removed addresses and cwd from the config 2017-10-04 11:49:05 -06:00
tigerbot
f25a0191bd changed config API to use an express router 2017-10-03 19:11:49 -06:00
tigerbot
3d3fac5087 simplified how the admin routes are handled 2017-10-03 17:26:44 -06:00
tigerbot
b8f282db79 fixed bug in promisifying network package 2017-10-02 15:37:58 -06:00
tigerbot
9e9b5ca9ad update DDNS to also use the specified list of domains 2017-09-29 15:29:47 -06:00
tigerbot
0dd20e4dfc removed tunnel from config and API and made DDNS responsible 2017-09-28 11:18:44 -06:00
tigerbot
5cc7e3f187 added loopback test before setting DNS records to local IP 2017-09-27 14:53:18 -06:00
tigerbot
83f72730a2 moved the DNS API calls to another file 2017-09-27 10:54:35 -06:00
tigerbot
8930a528bc moved some things related to DDNS into separate folder 2017-09-26 18:11:16 -06:00
55 changed files with 3603 additions and 4329 deletions

View File

@ -13,4 +13,5 @@
, "latedef": true
, "curly": true
, "trailing": true
, "esversion": 6
}

158
API.md
View File

@ -2,30 +2,154 @@
The API system is intended for use with Desktop and Mobile clients.
It must be accessed using one of the following domains as the Host header:
```
admin.invalid
localhost.admin.daplie.me
```
* localhost.alpha.daplie.me
* localhost.admin.daplie.me
* alpha.localhost.daplie.me
* admin.localhost.daplie.me
* localhost.daplie.invalid
All requests require an OAuth3 token in the request headers.
## Tunnel
## Tokens
### Check Status
* **URL** `/api/goldilocks@daplie.com/tunnel`
Some of the functionality of goldilocks requires the use of OAuth3 tokens to
perform tasks like setting DNS records. Management of these tokens can be done
using the following APIs.
### Get A Single Token
* **URL** `/api/goldilocks@daplie.com/tokens/:id`
* **Method** `GET`
* **Reponse**: The token matching the specified ID. Has the following properties.
* `id`: The hash used to identify the token. Based on several of the fields
inside the decoded token.
* `provider_uri`: The URI for the one who issued the token. Should be the same
as the `iss` field inside the decoded token.
* `client_uri`: The URI for the app authorized to use the token. Should be the
same as the `azp` field inside the decoded token.
* `scope`: The list of permissions granted by the token. Should be the same
as the `scp` field inside the decoded token.
* `access_token`: The encoded JWT.
* `token`: The decoded token.
### Get All Tokens
* **URL** `/api/goldilocks@daplie.com/tokens`
* **Method** `GET`
* **Reponse**: An array of the tokens stored. Each item looks the same as if it
had been requested individually.
### Save New Token
* **URL** `/api/goldilocks@daplie.com/tokens`
* **Method** `POST`
* **Reponse**: An object whose keys are the URLs for the tunnels, and whose
properties are arrays of the tunnel tokens.
* **Body**: An object similar to an OAuth3 session used by the javascript
library. The only important fields are `refresh_token` or `access_token`, and
`refresh_token` will be used before `access_token`. (This is because the
`access_token` usually expires quickly, making it meaningless to store.)
* **Reponse**: The response looks the same as a single GET request.
This route with return only the sessions started by the same user who is
checking the status.
### Delete Token
* **URL** `/api/goldilocks@daplie.com/tokens/:id`
* **Method** `DELETE`
* **Reponse**: Either `{"success":true}` or `{"success":false}`, depending on
whether the token was present before the request.
### Start Tunnel
* **URL** `/api/goldilocks@daplie.com/tunnel`
## Config
### Get All Settings
* **URL** `/api/goldilocks@daplie.com/config`
* **Method** `GET`
* **Reponse**: The JSON representation of the current config. See the [README.md](/README.md)
for the structure of the config.
### Get Group Setting
* **URL** `/api/goldilocks@daplie.com/config/:group`
* **Method** `GET`
* **Reponse**: The sub-object of the config relevant to the group specified in
the url (ie http, tls, tcp, etc.)
### Get Group Module List
* **URL** `/api/goldilocks@daplie.com/config/:group/modules`
* **Method** `GET`
* **Reponse**: The list of modules relevant to the group specified in the url
(ie http, tls, tcp, etc.)
### Get Specific Module
* **URL** `/api/goldilocks@daplie.com/config/:group/modules/:modId`
* **Method** `GET`
* **Reponse**: The module with the specified module ID.
### Get Domain Group
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId`
* **Method** `GET`
* **Reponse**: The domains specification with the specified domains ID.
### Get Domain Group Modules
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules`
* **Method** `GET`
* **Reponse**: An object containing all of the relevant modules for the group
of domains.
### Get Domain Group Module Category
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group`
* **Method** `GET`
* **Reponse**: A list of the specific category of modules for the group of domains.
### Get Specific Domain Group Module
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group/:modId`
* **Method** `GET`
* **Reponse**: The module with the specified module ID.
### Change Settings
* **URL** `/api/goldilocks@daplie.com/config`
* **URL** `/api/goldilocks@daplie.com/config/:group`
* **Method** `POST`
* **Body**: The changes to be applied on top of the current config. See the
[README.md](/README.md) for the settings. If modules or domains are specified
they are added to the current list.
* **Reponse**: The current config. If the group is specified in the URL it will
only be the config relevant to that group.
### Add Module
* **URL** `/api/goldilocks@daplie.com/config/:group/modules`
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group`
* **Method** `POST`
* **Body**: The module to be added. Can also be provided an array of modules
to add multiple modules in the same request.
* **Reponse**: The current list of modules.
### Add Domain Group
* **URL** `/api/goldilocks@daplie.com/config/domains`
* **Method** `POST`
* **Body**: The domains names and modules for the new domain group(s).
* **Reponse**: The current list of domain groups.
### Edit Module
* **URL** `/api/goldilocks@daplie.com/config/:group/modules/:modId`
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group/:modId`
* **Method** `PUT`
* **Body**: The new parameters for the module.
* **Reponse**: The editted module.
### Edit Domain Group
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId`
* **Method** `PUT`
* **Body**: The new domains names for the domains group. The module list cannot
be editted through this route.
* **Reponse**: The editted domain group.
### Remove Module
* **URL** `/api/goldilocks@daplie.com/config/:group/modules/:modId`
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group/:modId`
* **Method** `DELETE`
* **Reponse**: The list of modules.
### Remove Domain Group
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId`
* **Method** `DELETE`
* **Reponse**: The list of domain groups.
This route will use the stored token for the user matching the request
header to request a tunnel token from the audience of the stored token.
## Socks5 Proxy
@ -39,9 +163,9 @@ All requests require an OAuth3 token in the request headers.
### Start Proxy
* **URL** `/api/goldilocks@daplie.com/socks5`
* **Method** `POST`
* **Response**: Same response as for the `GET` resquest
* **Response**: Same response as for the `GET` request
### Stop Proxy
* **URL** `/api/goldilocks@daplie.com/socks5`
* **Method** `DELETE`
* **Response**: Same response as for the `GET` resquest
* **Response**: Same response as for the `GET` request

12
CHANGELOG Normal file
View File

@ -0,0 +1,12 @@
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
* loopback check less likely to fail / throw errors, will try again
* supports ddns using audience of token

41
LICENSE Normal file
View File

@ -0,0 +1,41 @@
Copyright 2017 Daplie, Inc
This is open source software; you can redistribute it and/or modify it under the
terms of either:
a) the "MIT License"
b) the "Apache-2.0 License"
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache-2.0 License Summary
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,3 +0,0 @@
Hello all. We make our source code available to view, but we retain copyright.
It's not because we're trying to be mean or anything, we just want to maintain our distribution channel.

568
README.md
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.coolaj86.com/coolaj86/goldilocks.js/raw/v1.1/installer/get.sh | bash
```
### git
```bash
git clone https://git.coolaj86.com/coolaj86/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.coolaj86.com:coolaj86/goldilocks.js#v1
# 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 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
```
@ -47,54 +81,97 @@ We have service support for
* launchd (macOS)
```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
-----
Goldilocks has several core systems, which all have their own configuration and some of which have modules:
Goldilocks has several core systems, which all have their own configuration and
some of which have modules:
```
* http
- static
- redirect
- proxy (reverse proxy)
* tls
- acme
- proxy (reverse proxy)
* tcp
- forward
* tunnel_server
* tunnel_client
* mdns
* [http](#http)
- [proxy (reverse proxy)](#httpproxy-how-to-reverse-proxy-ruby-python-etc)
- [static](#httpstatic-how-to-serve-a-web-page)
- [redirect](#httpredirect-how-to-redirect-urls)
* [tls](#tls)
- [proxy (reverse proxy)](#tlsproxy)
- [acme](#tlsacme)
* [tcp](#tcp)
- [proxy](#tcpproxy)
- [forward](#tcpforward)
* [udp](#udp)
- [forward](#udpforward)
* [domains](#domains)
* [tunnel_server](#tunnel_server)
* [DDNS](#ddns)
* [tunnel_client](#tunnel)
* [mDNS](#mdns)
* [socks5](#socks5)
* api
```
All modules require a `type` and an `id`, and any modules not defined inside the
`domains` system also require a `domains` field (with the exception of the `forward`
modules that require the `ports` field).
### http
The HTTP system handles plain http (TLS / SSL is handled by the tls system)
Example config:
```yml
http:
trust_proxy: true # allow localhost, 192.x, 10.x, 172.x, etc to set headers
allow_insecure: false # allow non-https even without proxy https headers
primary_domain: example.com # attempts to access via IP address will redirect here
# modules can be nested in domains
domains:
- names:
- example.com
# An array of modules that define how to handle incoming HTTP requests
modules:
- name: static
- type: static
domains:
- example.com
root: /srv/www/:hostname
```
# The configuration above could also be represented as follows:
### http.proxy - how to reverse proxy (ruby, python, etc)
The proxy module is for reverse proxying, typically to an application on the same machine.
(Though it can also reverse proxy to other devices on the local network.)
It has the following options:
```
address The DNS-resolvable hostname (or IP address) and port connected by `:` to proxy the request to.
Takes priority over host and port if they are also specified.
ex: locahost:3000
ex: 192.168.1.100:80
host The DNS-resolvable hostname (or IP address) of the system to which the request will be proxied.
Defaults to localhost if only the port is specified.
ex: localhost
ex: 192.168.1.100
port The port on said system to which the request will be proxied
ex: 3000
ex: 80
```
Example config:
```yml
http:
modules:
- name: static
- type: proxy
domains:
- example.com
root: /srv/www/:hostname
- api.example.com
host: 192.168.1.100
port: 80
- type: proxy
domains:
- www.example.com
address: 192.168.1.16:80
- type: proxy
domains:
- '*'
port: 3000
```
### http.static - how to serve a web page
@ -121,50 +198,20 @@ indexes An array of directories which should be have indexes served rather t
```
Example config:
```yml
http:
modules:
- name: static
- type: static
domains:
- example.com
root: /srv/www/:hostname
```
### http.proxy - how to reverse proxy (ruby, python, etc)
The proxy module is for reverse proxying, typically to an application on the same machine.
It has the following options:
```
host The DNS-resolvable hostname (or IP address) of the system to which the request will be proxied
ex: localhost
ex: 192.168.1.100
port The port on said system to which the request will be proxied
ex: 3000
ex: 80
```
Example config:
```yml
http:
modules:
- name: proxy
domains:
- example.com
host: localhost
port: 3000
```
### http.redirect - how to redirect URLs
The redirect module is for, you guessed it, redirecting URLs.
It has the following options:
```
status The HTTP status code to issue (301 is usual permanent redirect, 302 is temporary)
ex: 301
@ -182,11 +229,10 @@ to The new URL path which should be used.
```
Example config:
```yml
http:
modules:
- name: proxy
- type: proxy
domains:
- example.com
status: 301
@ -197,41 +243,14 @@ http:
### tls
The tls system handles encrypted connections, including fetching certificates,
and uses ServerName Indication (SNI) to determine if the connection should be handled
by the http system, a tls system module, or rejected.
It has the following options:
```
acme.email The default email address for ACME certificate issuance
ex: john.doe@example.com
acme.server The default ACME server to use
ex: https://acme-v01.api.letsencrypt.org/directory
ex: https://acme-staging.api.letsencrypt.org/directory
acme.challenge_type The default ACME challenge to request
ex: http-01, dns-01, tls-01
acme.approved_domains The domains for which to request certificates
ex: example.com
```
and uses ServerName Indication (SNI) to determine if the connection should be
handled by the http system, a tls system module, or rejected.
Example config:
```yml
tls:
acme:
email: 'joe.shmoe@example.com'
# IMPORTANT: Switch to in production 'https://acme-v01.api.letsencrypt.org/directory'
server: 'https://acme-staging.api.letsencrypt.org/directory'
challenge_type: 'http-01'
approved_domains:
- example.com
- example.net
modules:
- name: proxy
- type: proxy
domains:
- example.com
- example.net
@ -240,17 +259,44 @@ tls:
Certificates are saved to `~/acme`, which may be `/var/www/acme` if Goldilocks is run as the www-data user.
### tls.acme
### tls.proxy
The acme module overrides the acme defaults of the tls system and uses the same options except that `approved_domains`
(in favor of the domains in the scope of the module).
The proxy module routes the traffic based on the ServerName Indication (SNI) **without decrypting** it.
It has the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc).
Example config:
```yml
tls:
modules:
- name: acme
- type: proxy
domains:
- example.com
address: '127.0.0.1:5443'
```
### tls.acme
The acme module defines the setting used when getting new certificates.
It has the following options:
```
email The email address for ACME certificate issuance
ex: john.doe@example.com
server The ACME server to use
ex: https://acme-v01.api.letsencrypt.org/directory
ex: https://acme-staging.api.letsencrypt.org/directory
challenge_type The ACME challenge to request
ex: http-01, dns-01, tls-01
```
Example config:
```yml
tls:
modules:
- type: acme
domains:
- example.com
- example.net
@ -259,41 +305,25 @@ tls:
challenge_type: 'http-01'
```
### tls.proxy
The proxy module routes the traffic based on the ServerName Indication (SNI) **without decrypting** it.
It has the following options:
```
address The hostname (or IP) and port of the system or application that should receive the traffic
```
Example config:
```yml
tls:
modules:
- name: proxy
domains:
- example.com
address: '127.0.0.1:5443'
```
**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 all tcp network traffic **before decryption** and may use port numbers
The tcp system handles both *raw* and *tls-terminated* tcp network traffic
(see the _Note_ section below the example). It may use port numbers
or traffic sniffing to determine how the connection should be handled.
It has the following options:
```
bind An array of numeric ports on which to bind
ex: 80
```
Example Config
Example Config:
```yml
tcp:
bind:
@ -301,28 +331,102 @@ tcp:
- 80
- 443
modules:
- name: forward
- type: forward
ports:
- 22
address: '127.0.0.1:2222'
```
_Note_: When tcp traffic comes into goldilocks it will be tested against the tcp modules.
The connection may be handed to the TLS module if it appears to be a TLS/SSL/HTTPS connection
and if the tls module terminates the traffic, the connection will be sent back to the TLS module.
Due to the complexity of node.js' networking stack it is not currently possible to tell which
port tls-terminated traffic came from, so only the SNI header (serername / domain name) may be used for
modules matching terminated TLS.
### tcp.proxy
The proxy module routes traffic **after tls-termination** based on the servername (domain name)
contained in a SNI header. As such this only works to route TCP connections wrapped in a TLS stream.
It has the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc).
This is particularly useful for routing ssh and vpn traffic over tcp port 443 as wrapped TLS
connections in order to access one of your servers even when connecting from a harsh or potentially
misconfigured network environment (i.e. hotspots in public libraries and shopping malls).
Example config:
```yml
tcp:
modules:
- type: proxy
domains:
- ssh.example.com # Note: this domain would also listed in tls.acme.domains
host: localhost
port: 22
- type: proxy
domains:
- vpn.example.com # Note: this domain would also listed in tls.acme.domains
host: localhost
port: 1194
```
_Note_: In same cases network administrators purposefully block ssh and vpn connections using
Application Firewalls with DPI (deep packet inspection) enabled. You should read the ToS of the
network you are connected to to ensure that you aren't subverting policies that are purposefully
in place on such networks.
#### Using with ssh
In order to use this to route SSH connections you will need to use `ssh`'s
`ProxyCommand` option. For example to use the TLS certificate for `ssh.example.com`
to wrap an ssh connection you could use the following command:
```bash
ssh user@example.com -o ProxyCommand='openssl s_client -quiet -connect example.com:443 -servername ssh.example.com'
```
Alternatively you could add the following lines to your ssh config file.
```
Host example.com
ProxyCommand openssl s_client -quiet -connect example.com:443 -servername ssh.example.com
```
#### Using with OpenVPN
There are two strategies that will work well for you:
1) [Use ssh](https://redfern.me/tunneling-openvpn-through-ssh/) with the config above to reverse proxy tcp port 1194 to you.
```bash
ssh -L 1194:localhost:1194 example.com
```
2) [Use stunnel]https://serverfault.com/questions/675553/stunnel-vpn-traffic-and-ensure-it-looks-like-ssl-traffic-on-port-443/681497)
```
[openvpn-over-goldilocks]
client = yes
accept = 127.0.0.1:1194
sni = vpn.example.com
connect = example.com:443
```
3) [Use stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js) as described in the "tunnel_server" section below.
### tcp.forward
The forward module routes traffic based on port number **without decrypting** it.
It has the following options:
In addition to the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc),
the TCP forward modules also has the following options:
```
ports A numeric array of source ports
ex: 22
address The destination hostname and port
ex: 127.0.0.1:2222
```
Example Config
Example Config:
```yml
tcp:
bind:
@ -330,10 +434,97 @@ tcp:
- 80
- 443
modules:
- name: forward
- type: forward
ports:
- 22
address: '127.0.0.1:2222'
port: 2222
```
### udp
The udp system handles all udp network traffic. It currently only supports
forwarding the messages without any examination.
It has the following options:
```
bind An array of numeric ports on which to bind
ex: 53
```
Example Config:
```yml
udp:
bind:
- 53
modules:
- type: forward
ports:
- 53
address: '127.0.0.1:8053'
```
### udp.forward
The forward module routes traffic based on port number **without decrypting** it.
It has the same options as the [TCP forward module](#tcpforward).
Example Config:
```yml
udp:
bind:
- 53
modules:
- type: forward
ports:
- 53
address: '127.0.0.1:8053'
```
### domains
To reduce repetition defining multiple modules that operate on the same domain
name the `domains` field can define multiple modules of multiple types for a
single list of names. The modules defined this way do not need to have their
own `domains` field. Note that the [tcp.forward](#tcpforward) module is not
allowed in a domains group since its routing is not based on domains.
Example Config
```yml
domains:
- names:
- example.com
- www.example.com
- api.example.com
modules:
tls:
- type: acme
email: joe.schmoe@example.com
challenge_type: 'http-01'
http:
- type: redirect
from: /deprecated/path
to: /new/path
- type: proxy
port: 3000
dns:
- type: 'dns@oauth3.org'
token_id: user_token_id
- names:
- ssh.example.com
modules:
tls:
- type: acme
email: john.smith@example.com
challenge_type: 'http-01'
tcp:
- type: proxy
port: 22
dns:
- type: 'dns@oauth3.org'
token_id: user_token_id
```
@ -363,66 +554,50 @@ tunnel_server:
- 'api.tunnel.example.com'
```
### tunnel
### DDNS
The tunnel client is meant to be run from behind a firewalls, carrier-grade NAT,
or otherwise inaccessible devices to allow them to be accessed publicly on the
internet.
The DDNS module watches the network environment of the unit and makes sure the
device is always accessible on the internet using the domains listed in the
config. If the device has a public address or if it can automatically set up
port forwarding the device will periodically check its public address to ensure
the DNS records always point to it. Otherwise it will to connect to a tunnel
server and set the DNS records to point to that server.
It has no options per se, but is rather a list of tokens that can be used to
connect to tunnel servers. If the token does not have an `aud` field it must be
provided in an object with the token provided in the `jwt` field and the tunnel
server url provided in the `tunnelUrl` field.
The `loopback` setting specifies how the unit will check its public IP address
and whether connections can reach it. Currently only `tunnel@oauth3.org` is
supported. If the loopback setting is not defined it will default to using
`oauth3.org`.
Example config:
The `tunnel` setting can be used to specify how to connect to the tunnel.
Currently only `tunnel@oauth3.org` is supported. The token specified in the
`tunnel` setting will be used to acquire the tokens that are used directly with
the tunnel server. If the tunnel setting is not defined it will default to try
using the tokens in the modules for the relevant domains.
```yml
If a particular DDNS module has been disabled the device will still try to set
up port forwarding (and connect to a tunnel if that doesn't work), but the DNS
records will not be updated to point to the device. This is to allow a setup to
be tested before transitioning services between devices.
```yaml
ddns:
disabled: false
loopback:
type: 'tunnel@oauth3.org'
domain: oauth3.org
tunnel:
- 'some.jwt_encoded.token'
- jwt: 'other.jwt_encoded.token'
tunnelUrl: 'wss://api.tunnel.example.com/'
type: 'tunnel@oauth3.org'
token_id: user_token_id
modules:
- type: 'dns@oauth3.org'
token_id: user_token_id
domains:
- www.example.com
- api.example.com
- test.example.com
```
**NOTE**: The more common way to use the tunnel with goldilocks is to use the
API to have goldilocks get a token from `oauth3.org`. In order to do this you
will need to have initialized goldilocks with a token that has the `dns` and
`domains` scopes. This is probably easiest to do with the `daplie-desktop-app`,
which will also get the first tunnel token for you.
**If you want to add more domains** to handle on your device while using the tunnel
you will need to manually get a new token that will tell the tunnel server to
deliver the requests to the new domain(s) to your device. The first step in this
is to attach the new domains to your device. To get the name of the device you
can use the `config` API, but it's probably easiest to `ssh` onto the device and
get the hostname. You can also use the daplie cli tool to see what device name
your other domains are routed to.
```bash
# for every new domain you want to route attach the domain to your device
daplie devices:attach -n $new_domain -d $device_name
```
After that step you will need to use the API to get goldilocks to get a new token
that includes the new domains you attached. It is also recommended but not
required to remove the older token with the incomplete list of domains. Run the
following commands from the unit.
```bash
# remove the old token
rm /opt/goldilocks/lib/node_modules/goldilocks/var/tokens.json
# set the "refresh_token" to a bash variable `token`
TOKEN=$(python -mjson.tool /opt/goldilocks/lib/node_modules/goldilocks/var/owners.json | sed -n 's|\s*"refresh_token": "\(.*\)",|\1|p')
# tell goldilocks to get a new tunnel token
curl -H "authorization: bearer $TOKEN" -X POST https://localhost.admin.daplie.me/api/goldilocks@daplie.com/tunnel
```
### ddns
TODO
### mdns
### mDNS
enabled by default
@ -441,7 +616,7 @@ mdns:
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
```
@ -465,15 +640,14 @@ See [API.md](/API.md)
TODO
----
* http - nowww module
* http - Allow match styles of `www.*`, `*`, and `*.example.com` equally
* http - redirect based on domain name (not just path)
* 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
* sys - handle SIGHUP
* sys - `curl https://daplie.me/goldilocks | bash -s example.com`
* oauth3 - `example.com/.well-known/domains@oauth3.org/directives.json`
* oauth3 - commandline questionnaire
* modules - use consistent conventions (i.e. address vs host + port)
* tls - tls.acme vs tls.modules.acme
* tls - forward should be able to match on source port to reach different destination ports
* [ ] http - nowww module
* [ ] http - Allow match styles of `www.*`, `*`, and `*.example.com` equally
* [ ] http - redirect based on domain name (not just path)
* [ ] 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
* [ ] sys - `curl https://coolaj86.com/goldilocks | bash -s example.com`
* [ ] oauth3 - `example.com/.well-known/domains@oauth3.org/directives.json`
* [ ] oauth3 - commandline questionnaire
* [x] modules - use consistent conventions (i.e. address vs host + port)
* [x] tls - tls.acme vs tls.modules.acme
* [ ] tls - forward should be able to match on source port to reach different destination ports

View File

@ -8,6 +8,7 @@ if (!cluster.isMaster) {
return;
}
var crypto = require('crypto');
var PromiseA = require('bluebird');
var fs = PromiseA.promisifyAll(require('fs'));
var configStorage;
@ -25,7 +26,158 @@ function mergeSettings(orig, changes) {
}
});
}
function createStorage(filename, filetype) {
function fixRawConfig(config) {
var updated = false;
// First converge all of the `bind` properties for protocols that are on top
// of TCP to `tcp.bind`.
if (config.tcp && config.tcp.bind && !Array.isArray(config.tcp.bind)) {
config.tcp.bind = [ config.tcp.bind ];
updated = true;
}
if (config.http && config.http.bind) {
config.tcp = config.tcp || { bind: [] };
config.tcp.bind = (config.tcp.bind || []).concat(config.http.bind);
delete config.http.bind;
updated = true;
}
if (config.tls && config.tls.bind) {
config.tcp = config.tcp || { bind: [] };
config.tcp.bind = (config.tcp.bind || []).concat(config.tls.bind);
delete config.tls.bind;
updated = true;
}
// Then we rename dns to udp since the only thing we currently do with those
// modules is proxy the packets without inspecting them at all.
if (config.dns) {
config.udp = config.dns;
delete config.dns;
updated = true;
}
// Convert all 'proxy' UDP modules to 'forward' modules that specify which
// incoming ports are relevant. Primarily to make 'proxy' modules consistent
// in needing relevant domain names.
if (config.udp && !Array.isArray(config.udp.bind)) {
config.udp.bind = [].concat(config.udp.bind || []);
updated = true;
}
if (config.udp && config.udp.modules) {
if (!config.udp.bind.length || !Array.isArray(config.udp.modules)) {
delete config.udp.modules;
updated = true;
} else {
config.udp.modules.forEach(function (mod) {
if (mod.type === 'proxy') {
mod.type = 'forward';
mod.ports = config.udp.bind.slice();
updated = true;
}
});
}
}
// This we take the old way of defining ACME options and put them into a tls module.
if (config.tls) {
var oldPropMap = {
email: 'email'
, acme_directory_url: 'server'
, challenge_type: 'challenge_type'
, servernames: 'approved_domains'
};
if (Object.keys(oldPropMap).some(config.tls.hasOwnProperty, config.tls)) {
updated = true;
if (config.tls.acme) {
console.warn('TLS config has `acme` field and old style definitions');
} else {
config.tls.acme = {};
Object.keys(oldPropMap).forEach(function (oldKey) {
if (config.tls[oldKey]) {
config.tls.acme[oldPropMap[oldKey]] = config.tls[oldKey];
}
});
}
}
if (config.tls.acme) {
updated = true;
config.tls.acme.domains = config.tls.acme.approved_domains;
delete config.tls.acme.approved_domains;
config.tls.modules = config.tls.modules || [];
config.tls.modules.push(Object.assign({}, config.tls.acme, {type: 'acme'}));
delete config.tls.acme;
}
}
// Then we make sure all modules have an ID and type, and makes sure all domains
// are in the right spot and also have an ID.
function updateModules(list) {
if (!Array.isArray(list)) {
return;
}
list.forEach(function (mod) {
if (!mod.id) {
mod.id = crypto.randomBytes(4).toString('hex');
updated = true;
}
if (mod.name) {
mod.type = mod.type || mod.name;
delete mod.name;
updated = true;
}
});
}
function moveDomains(name) {
if (!config[name].domains) {
return;
}
updated = true;
var domList = config[name].domains;
delete config[name].domains;
if (!Array.isArray(domList)) {
return;
}
if (!Array.isArray(config.domains)) {
config.domains = [];
}
domList.forEach(function (dom) {
updateModules(dom.modules);
var strDoms = dom.names.slice().sort().join(',');
var added = config.domains.some(function (existing) {
if (strDoms !== existing.names.slice().sort().join(',')) {
return;
}
existing.modules = existing.modules || {};
existing.modules[name] = (existing.modules[name] || []).concat(dom.modules);
return true;
});
if (added) {
return;
}
var newDom = {
id: crypto.randomBytes(4).toString('hex')
, names: dom.names
, modules: {}
};
newDom.modules[name] = dom.modules;
config.domains.push(newDom);
});
}
[ 'udp', 'tcp', 'http', 'tls' ].forEach(function (key) {
if (!config[key]) {
return;
}
updateModules(config[key].modules);
moveDomains(key);
});
return updated;
}
async function createStorage(filename, filetype) {
var recase = require('recase').create({});
var snakeCopy = recase.snakeCopy.bind(recase);
var camelCopy = recase.camelCopy.bind(recase);
@ -40,13 +192,25 @@ function createStorage(filename, filetype) {
dump = yaml.safeDump;
}
function read() {
return fs.readFileAsync(filename).then(parse).catch(function (err) {
async function read() {
var text;
try {
text = await fs.readFileAsync(filename);
} catch (err) {
if (err.code === 'ENOENT') {
return {};
}
return PromiseA.reject(err);
});
throw err;
}
var rawConfig = parse(text);
if (fixRawConfig(rawConfig)) {
await fs.writeFileAsync(filename, dump(rawConfig));
text = await fs.readFileAsync(filename);
rawConfig = parse(text);
}
return rawConfig;
}
var result = {
@ -76,51 +240,53 @@ function createStorage(filename, filetype) {
};
return result;
}
function checkConfigLocation(cwd, configFile) {
async function checkConfigLocation(cwd, configFile) {
cwd = cwd || process.cwd();
var path = require('path');
var filename;
var filename, text;
var prom;
if (configFile) {
filename = path.resolve(cwd, configFile);
prom = fs.readFileAsync(filename)
.catch(function (err) {
try {
text = await fs.readFileAsync(filename);
} catch (err) {
if (err.code !== 'ENOENT') {
return PromiseA.reject(err);
throw err;
}
if (path.extname(filename) === '.json') {
return '{}';
}
return '';
})
;
return { name: filename, type: 'json' };
} else {
prom = PromiseA.reject('blah')
.catch(function () {
filename = path.resolve(cwd, 'goldilocks.yml');
return fs.readFileAsync(filename);
})
.catch(function () {
filename = path.resolve(cwd, 'goldilocks.json');
return fs.readFileAsync(filename);
})
.catch(function () {
filename = path.resolve(cwd, 'etc/goldilocks/goldilocks.yml');
return fs.readFileAsync(filename);
})
.catch(function () {
filename = '/etc/goldilocks/goldilocks.yml';
return fs.readFileAsync(filename);
})
.catch(function () {
filename = path.resolve(cwd, 'goldilocks.yml');
return '';
})
;
return { name: filename, type: 'yaml' };
}
}
} else {
// Note that `path.resolve` can handle both relative and absolute paths.
var defLocations = [
path.resolve(cwd, 'goldilocks.yml')
, path.resolve(cwd, 'goldilocks.json')
, path.resolve(cwd, 'etc/goldilocks/goldilocks.yml')
, '/etc/goldilocks/goldilocks.yml'
];
var ind;
for (ind = 0; ind < defLocations.length; ind += 1) {
try {
text = await fs.readFileAsync(defLocations[ind]);
filename = defLocations[ind];
break;
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
}
if (!filename) {
filename = defLocations[0];
text = '';
}
}
return prom.then(function (text) {
try {
JSON.parse(text);
return { name: filename, type: 'json' };
@ -132,89 +298,50 @@ function checkConfigLocation(cwd, configFile) {
} catch (err) {}
throw new Error('Could not load "' + filename + '" as JSON nor YAML');
});
}
function createConfigStorage(args) {
return checkConfigLocation(args.cwd, args.config)
.then(function (result) {
async function createConfigStorage(args) {
var result = await checkConfigLocation(args.cwd, args.config);
console.log('config file', result.name, 'is of type', result.type);
configStorage = createStorage(result.name, result.type);
configStorage = await createStorage(result.name, result.type);
return configStorage.read();
})
;
}
var tcpProm;
function fillConfig(config, args) {
config.debug = config.debug || args.debug;
if (!config.dns) {
config.dns = { bind: [ 53 ], modules: [{ name: 'proxy', port: 3053 }] };
}
if (!config.ddns) {
config.ddns = { enabled: false };
}
// Use Object.assign to add any properties needed but not defined in the mdns config.
// It will first copy the defaults into an empty object, then copy any real config over that.
var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 };
config.mdns = Object.assign({}, mdnsDefaults, config.mdns);
config.socks5 = config.socks5 || { enabled: false };
if (!config.tcp) {
config.tcp = {};
}
if (!config.http) {
config.http = { modules: [{ name: 'proxy', domains: ['*'], port: 3000 }] };
}
if (!config.tls) {
config.tls = {};
}
if (!config.tls.acme && (args.email || args.agreeTos)) {
config.tls.acme = {};
}
if (typeof args.agreeTos === 'string') {
config.tls.acme.approvedDomains = args.agreeTos.split(',');
}
if (args.email) {
config.email = args.email;
config.tls.acme.email = args.email;
// Use Object.assign to copy any real config values over the default values so we can
// easily make sure all the fields we need exist .
var mdnsDefaults = { disabled: false, port: 5353, broadcast: '224.0.0.251', ttl: 300 };
config.mdns = Object.assign(mdnsDefaults, config.mdns);
if (!Array.isArray(config.domains)) {
config.domains = [];
}
// maybe this should not go in config... but be ephemeral in some way?
config.cwd = args.cwd || config.cwd || process.cwd();
var ipaddr = require('ipaddr.js');
var addresses = [];
var ifaces = require('../lib/local-ip.js').find();
Object.keys(ifaces).forEach(function (ifacename) {
var iface = ifaces[ifacename];
iface.ipv4.forEach(function (ip) {
addresses.push(ip);
});
iface.ipv6.forEach(function (ip) {
addresses.push(ip);
});
});
addresses.sort(function (a, b) {
if (a.family !== b.family) {
return 'IPv4' === a.family ? 1 : -1;
function fillComponent(name, fillBind) {
if (!config[name]) {
config[name] = {};
}
if (!Array.isArray(config[name].modules)) {
config[name].modules = [];
}
return a.address > b.address ? 1 : -1;
});
if (fillBind && !Array.isArray(config[name].bind)) {
config[name].bind = [];
}
}
fillComponent('udp', true);
fillComponent('tcp', true);
fillComponent('http', false);
fillComponent('tls', false);
fillComponent('ddns', false);
addresses.forEach(function (addr) {
addr.range = ipaddr.parse(addr.address).range();
});
// TODO maybe move to config.state.addresses (?)
config.addresses = addresses;
config.device = { hostname: require('os').hostname() };
config.tunnel = args.tunnel || config.tunnel;
if (Array.isArray(config.tcp.bind)) {
if (Array.isArray(config.tcp.bind) && config.tcp.bind.length) {
return PromiseA.resolve(config);
}
@ -297,6 +424,9 @@ function run(args) {
// TODO spin up multiple workers
// TODO use greenlock-cluster
cluster.fork();
}).catch(function (err) {
console.error(err);
process.exit(1);
})
;
}
@ -310,9 +440,7 @@ function readEnv(args) {
} catch (err) {}
var env = {
tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true
, email: process.env.GOLDILOCKS_EMAIL
, cwd: process.env.GOLDILOCKS_HOME || process.cwd()
cwd: process.env.GOLDILOCKS_HOME || process.cwd()
, debug: process.env.GOLDILOCKS_DEBUG && true
};
@ -323,10 +451,7 @@ var program = require('commander');
program
.version(require('../package.json').version)
.option('--agree-tos [url1,url2]', "agree to all Terms of Service for Daplie, Let's Encrypt, etc (or specific URLs only)")
.option('-c --config <file>', 'Path to config file (Goldilocks.json or Goldilocks.yml) example: --config /etc/goldilocks/Goldilocks.json')
.option('--tunnel [token]', 'Turn tunnel on. This will enter interactive mode for login if no token is specified.')
.option('--email <email>', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.")
.option('--debug', "Enable debug output")
.parse(process.argv);

View File

@ -4,90 +4,103 @@ tcp:
- 80
- 443
modules:
- name: forward
- type: forward
ports:
- 22
address: '127.0.0.1:8022'
# tunnel: jwt
# tunnel:
# - jwt1
# - jwt2
udp:
bind:
- 53
modules:
- type: forward
ports:
- 53
port: 5353
# default host is localhost
tunnel_server:
secret: abc123
servernames:
- 'tunnel.localhost.com'
tls:
acme:
email: 'joe.shmoe@example.com'
server: 'https://acme-staging.api.letsencrypt.org/directory'
challenge_type: 'http-01'
approved_domains:
- localhost.baz.daplie.me
- localhost.beta.daplie.me
domains:
- names:
- localhost.gamma.daplie.me
modules:
- name: proxy
address: '127.0.0.1:6443'
- names:
- beta.localhost.daplie.me
- baz.localhost.daplie.me
modules:
- name: acme
email: 'owner@example.com'
challenge_type: 'tls-sni-01'
# default server is 'https://acme-v01.api.letsencrypt.org/directory'
modules:
- name: proxy
- type: proxy
domains:
- localhost.bar.daplie.me
- localhost.foo.daplie.me
address: '127.0.0.1:5443'
- name: acme
- type: acme
domains:
- '*.localhost.daplie.me'
email: 'guest@example.com'
challenge_type: 'http-01'
domains:
- foo.localhost.daplie.me
- gamma.localhost.daplie.me
http:
trust_proxy: true
allow_insecure: false
primary_domain: localhost.foo.daplie.me
domains:
- names:
- localhost.baz.daplie.me
modules:
- name: redirect
from: /nowhere/in/particular
to: /just/an/example
- name: proxy
port: 3001
primary_domain: localhost.daplie.me
modules:
- name: redirect
- type: redirect
domains:
- localhost.beta.daplie.me
status: 301
from: /old/path/*/other/*
to: https://example.com/path/new/:2/something/:1
- name: proxy
- type: proxy
domains:
- localhost.daplie.me
host: localhost
port: 4000
- name: static
- type: static
domains:
- '*.localhost.daplie.me'
root: '/srv/www/:hostname'
domains:
- names:
- localhost.gamma.daplie.me
modules:
tls:
- type: proxy
port: 6443
- names:
- beta.localhost.daplie.me
- baz.localhost.daplie.me
modules:
tls:
- type: acme
email: 'owner@example.com'
challenge_type: 'tls-sni-01'
# default server is 'https://acme-v01.api.letsencrypt.org/directory'
http:
- type: redirect
from: /nowhere/in/particular
to: /just/an/example
- type: proxy
address: '127.0.0.1:3001'
mdns:
disabled: false
port: 5353
broadcast: '224.0.0.251'
ttl: 300
tunnel_server:
secret: abc123
servernames:
- 'tunnel.localhost.com'
ddns:
loopback:
type: 'tunnel@oauth3.org'
domain: oauth3.org
tunnel:
type: 'tunnel@oauth3.org'
token: user_token_id
modules:
- type: 'dns@oauth3.org'
token: user_token_id
domains:
- www.example.com
- api.example.com
- test.example.com

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.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)"
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.coolaj86.com/coolaj86/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,37 @@
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"
$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_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 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 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

150
installer/install.sh Normal file
View File

@ -0,0 +1,150 @@
#!/bin/bash
set -e
set -u
### IMPORTANT ###
### VERSION ###
my_name=goldilocks
my_app_pkg_name=com.coolaj86.goldilocks.web
my_app_ver="v1.1"
my_azp_oauth3_ver="v1.2.3"
export NODE_VERSION="v8.9.3"
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.coolaj86.com/coolaj86/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"
source ./installer/install-system-service.sh
$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
# 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

@ -21,6 +21,7 @@ module.exports.create = function (deps, conf) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', methods.join(', '));
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method.toUpperCase() === 'OPTIONS') {
res.setHeader('Allow', methods.join(', '));
@ -38,6 +39,26 @@ module.exports.create = function (deps, conf) {
return true;
}
}
function makeCorsHandler(methods) {
return function corsHandler(req, res, next) {
if (!handleCors(req, res, methods)) {
next();
}
};
}
function handlePromise(req, res, prom) {
prom.then(function (result) {
res.send(deps.recase.snakeCopy(result));
}).catch(function (err) {
if (conf.debug) {
console.log(err);
}
res.statusCode = err.statusCode || 500;
err.message = err.message || err.toString();
res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
});
}
function isAuthorized(req, res, fn) {
var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, ''));
@ -139,7 +160,10 @@ module.exports.create = function (deps, conf) {
;
}
return {
// This object contains all of the API endpoints written before we changed how
// the API routing is handled. Eventually it will hopefully disappear, but for
// now we're focusing on the things that need changing more.
var oldEndPoints = {
init: function (req, res) {
if (handleCors(req, res, ['GET', 'POST'])) {
return;
@ -235,55 +259,6 @@ module.exports.create = function (deps, conf) {
});
}
, tunnel: function (req, res) {
if (handleCors(req, res)) {
return;
}
isAuthorized(req, res, function () {
if ('POST' !== req.method) {
res.setHeader('Content-Type', 'application/json');
return deps.tunnelClients.get(req.userId).then(function (result) {
res.end(JSON.stringify(result));
}, function (err) {
res.statusCode = 500;
res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } }));
});
}
return deps.storage.owners.get(req.userId).then(function (session) {
return deps.tunnelClients.start(session).then(function () {
res.setHeader('Content-Type', 'application/json;');
res.end(JSON.stringify({ success: true }));
}, function (err) {
res.setHeader('Content-Type', 'application/json;');
res.statusCode = 500;
res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } }));
});
});
});
}
, config: function (req, res) {
if (handleCors(req, res)) {
return;
}
isAuthorized(req, res, function () {
if ('POST' !== req.method) {
res.setHeader('Content-Type', 'application/json;');
res.end(JSON.stringify(deps.recase.snakeCopy(conf)));
return;
}
jsonParser(req, res, function () {
console.log('config POST body', req.body);
// Since we are sending the changes to another process we don't really
// have a good way of seeing if it worked, so always report success
deps.storage.config.save(req.body);
res.setHeader('Content-Type', 'application/json;');
res.end('{"success":true}');
});
});
}
, request: function (req, res) {
if (handleCors(req, res, '*')) {
return;
@ -315,29 +290,6 @@ module.exports.create = function (deps, conf) {
});
});
}
, loopback: function (req, res) {
if (handleCors(req, res, 'GET')) {
return;
}
isAuthorized(req, res, function () {
var prom;
var query = require('querystring').parse(require('url').parse(req.url).query);
if (query.provider) {
prom = deps.loopback(query.provider);
} else {
prom = deps.storage.owners.get(req.userId).then(function (session) {
return deps.loopback(session.token.aud);
});
}
res.setHeader('Content-Type', 'application/json');
prom.then(function (result) {
res.end(JSON.stringify(result));
}, function (err) {
res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
});
});
}
, paywall_check: function (req, res) {
if (handleCors(req, res, 'GET')) {
return;
@ -381,4 +333,253 @@ module.exports.create = function (deps, conf) {
});
}
};
function handleOldApis(req, res, next) {
if (typeof oldEndPoints[req.params.name] === 'function') {
oldEndPoints[req.params.name](req, res);
} else {
next();
}
}
var config = { restful: {} };
config.restful.readConfig = function (req, res, next) {
var part = new (require('./config').ConfigChanger)(conf);
if (req.params.group) {
part = part[req.params.group];
}
if (part && req.params.domId) {
part = part.domains.findId(req.params.domId);
}
if (part && req.params.mod) {
part = part[req.params.mod];
}
if (part && req.params.modGrp) {
part = part[req.params.modGrp];
}
if (part && req.params.modId) {
part = part.findId(req.params.modId);
}
if (part) {
res.send(deps.recase.snakeCopy(part));
} else {
next();
}
};
config.save = function (changer) {
var errors = changer.validate();
if (errors.length) {
throw Object.assign(new Error(), errors[0], {statusCode: 400});
}
return deps.storage.config.save(changer);
};
config.restful.saveBaseConfig = function (req, res, next) {
console.log('config POST body', JSON.stringify(req.body));
if (req.params.group === 'domains') {
next();
return;
}
var promise = deps.PromiseA.resolve().then(function () {
var update;
if (req.params.group) {
update = {};
update[req.params.group] = req.body;
} else {
update = req.body;
}
var changer = new (require('./config').ConfigChanger)(conf);
changer.update(update);
return config.save(changer);
}).then(function (newConf) {
if (req.params.group) {
return newConf[req.params.group];
}
return newConf;
});
handlePromise(req, res, promise);
};
config.extractModList = function (changer, params) {
var err;
if (params.domId) {
var dom = changer.domains.find(function (dom) {
return dom.id === params.domId;
});
if (!dom) {
err = new Error("no domain with ID '"+params.domId+"'");
} else if (!dom.modules[params.group]) {
err = new Error("domains don't contain '"+params.group+"' modules");
} else {
return dom.modules[params.group];
}
} else {
if (!changer[params.group] || !changer[params.group].modules) {
err = new Error("'"+params.group+"' is not a valid settings group or doesn't support modules");
} else {
return changer[params.group].modules;
}
}
err.statusCode = 404;
throw err;
};
config.restful.createModule = function (req, res, next) {
if (req.params.group === 'domains') {
next();
return;
}
var promise = deps.PromiseA.resolve().then(function () {
var changer = new (require('./config').ConfigChanger)(conf);
var modList = config.extractModList(changer, req.params);
var update = req.body;
if (!Array.isArray(update)) {
update = [ update ];
}
update.forEach(modList.add, modList);
return config.save(changer);
}).then(function (newConf) {
return config.extractModList(newConf, req.params);
});
handlePromise(req, res, promise);
};
config.restful.updateModule = function (req, res, next) {
if (req.params.group === 'domains') {
next();
return;
}
var promise = deps.PromiseA.resolve().then(function () {
var changer = new (require('./config').ConfigChanger)(conf);
var modList = config.extractModList(changer, req.params);
modList.update(req.params.modId, req.body);
return config.save(changer);
}).then(function (newConf) {
return config.extractModule(newConf, req.params).find(function (mod) {
return mod.id === req.params.modId;
});
});
handlePromise(req, res, promise);
};
config.restful.removeModule = function (req, res, next) {
if (req.params.group === 'domains') {
next();
return;
}
var promise = deps.PromiseA.resolve().then(function () {
var changer = new (require('./config').ConfigChanger)(conf);
var modList = config.extractModList(changer, req.params);
modList.remove(req.params.modId);
return config.save(changer);
}).then(function (newConf) {
return config.extractModList(newConf, req.params);
});
handlePromise(req, res, promise);
};
config.restful.createDomain = function (req, res) {
var promise = deps.PromiseA.resolve().then(function () {
var changer = new (require('./config').ConfigChanger)(conf);
var update = req.body;
if (!Array.isArray(update)) {
update = [ update ];
}
update.forEach(changer.domains.add, changer.domains);
return config.save(changer);
}).then(function (newConf) {
return newConf.domains;
});
handlePromise(req, res, promise);
};
config.restful.updateDomain = function (req, res) {
var promise = deps.PromiseA.resolve().then(function () {
if (req.body.modules) {
throw Object.assign(new Error('do not add modules with this route'), {statusCode: 400});
}
var changer = new (require('./config').ConfigChanger)(conf);
changer.domains.update(req.params.domId, req.body);
return config.save(changer);
}).then(function (newConf) {
return newConf.domains.find(function (dom) {
return dom.id === req.params.domId;
});
});
handlePromise(req, res, promise);
};
config.restful.removeDomain = function (req, res) {
var promise = deps.PromiseA.resolve().then(function () {
var changer = new (require('./config').ConfigChanger)(conf);
changer.domains.remove(req.params.domId);
return config.save(changer);
}).then(function (newConf) {
return newConf.domains;
});
handlePromise(req, res, promise);
};
var tokens = { restful: {} };
tokens.restful.getAll = function (req, res) {
handlePromise(req, res, deps.storage.tokens.all());
};
tokens.restful.getOne = function (req, res) {
handlePromise(req, res, deps.storage.tokens.get(req.params.id));
};
tokens.restful.save = function (req, res) {
handlePromise(req, res, deps.storage.tokens.save(req.body));
};
tokens.restful.revoke = function (req, res) {
var promise = deps.storage.tokens.remove(req.params.id).then(function (success) {
return {success: success};
});
handlePromise(req, res, promise);
};
var app = require('express')();
// Handle all of the API endpoints using the old definition style, and then we can
// add middleware without worrying too much about the consequences to older code.
app.use('/:name', handleOldApis);
// Not all routes support all of these methods, but not worth making this more specific
app.use('/', makeCorsHandler(['GET', 'POST', 'PUT', 'DELETE']), isAuthorized, jsonParser);
app.get( '/config', config.restful.readConfig);
app.get( '/config/:group', config.restful.readConfig);
app.get( '/config/:group/:mod(modules)/:modId?', config.restful.readConfig);
app.get( '/config/domains/:domId/:mod(modules)?', config.restful.readConfig);
app.get( '/config/domains/:domId/:mod(modules)/:modGrp/:modId?', config.restful.readConfig);
app.post( '/config', config.restful.saveBaseConfig);
app.post( '/config/:group', config.restful.saveBaseConfig);
app.post( '/config/:group/modules', config.restful.createModule);
app.put( '/config/:group/modules/:modId', config.restful.updateModule);
app.delete('/config/:group/modules/:modId', config.restful.removeModule);
app.post( '/config/domains/:domId/modules/:group', config.restful.createModule);
app.put( '/config/domains/:domId/modules/:group/:modId', config.restful.updateModule);
app.delete('/config/domains/:domId/modules/:group/:modId', config.restful.removeModule);
app.post( '/config/domains', config.restful.createDomain);
app.put( '/config/domains/:domId', config.restful.updateDomain);
app.delete('/config/domains/:domId', config.restful.removeDomain);
app.get( '/tokens', tokens.restful.getAll);
app.get( '/tokens/:id', tokens.restful.getOne);
app.post( '/tokens', tokens.restful.save);
app.delete('/tokens/:id', tokens.restful.revoke);
return app;
};

398
lib/admin/config.js Normal file
View File

@ -0,0 +1,398 @@
'use strict';
var validator = new (require('jsonschema').Validator)();
var recase = require('recase').create({});
var portSchema = { type: 'number', minimum: 1, maximum: 65535 };
var moduleSchemas = {
// the proxy module is common to basically all categories.
proxy: {
type: 'object'
, oneOf: [
{ required: [ 'address' ] }
, { required: [ 'port' ] }
]
, properties: {
address: { type: 'string' }
, host: { type: 'string' }
, port: portSchema
}
}
// redirect and static modules are for HTTP
, redirect: {
type: 'object'
, required: [ 'to', 'from' ]
, properties: {
to: { type: 'string'}
, from: { type: 'string'}
, status: { type: 'integer', minimum: 1, maximum: 999 }
, }
}
, static: {
type: 'object'
, required: [ 'root' ]
, properties: {
root: { type: 'string' }
}
}
// the acme module is for TLS
, acme: {
type: 'object'
, required: [ 'email' ]
, properties: {
email: { type: 'string' }
, server: { type: 'string' }
, challenge_type: { type: 'string' }
}
}
// the dns control modules for DDNS
, 'dns@oauth3.org': {
type: 'object'
, required: [ 'token_id' ]
, properties: {
token_id: { type: 'string' }
}
}
};
// forward is basically the same as proxy, but specifies the relevant incoming port(s).
// only allows for the raw transport layers (TCP/UDP)
moduleSchemas.forward = JSON.parse(JSON.stringify(moduleSchemas.proxy));
moduleSchemas.forward.required = [ 'ports' ];
moduleSchemas.forward.properties.ports = { type: 'array', items: portSchema };
Object.keys(moduleSchemas).forEach(function (name) {
var schema = moduleSchemas[name];
schema.id = '/modules/'+name;
schema.required = ['id', 'type'].concat(schema.required || []);
schema.properties.id = { type: 'string' };
schema.properties.type = { type: 'string', const: name };
validator.addSchema(schema, schema.id);
});
function addDomainRequirement(itemSchema) {
var result = Object.assign({}, itemSchema);
result.required = (result.required || []).concat('domains');
result.properties = Object.assign({}, result.properties);
result.properties.domains = { type: 'array', items: { type: 'string' }, minLength: 1};
return result;
}
function toSchemaRef(name) {
return { '$ref': '/modules/'+name };
}
var moduleRefs = {
http: [ 'proxy', 'static', 'redirect' ].map(toSchemaRef)
, tls: [ 'proxy', 'acme' ].map(toSchemaRef)
, tcp: [ 'forward' ].map(toSchemaRef)
, udp: [ 'forward' ].map(toSchemaRef)
, ddns: [ 'dns@oauth3.org' ].map(toSchemaRef)
};
// TCP is a bit special in that it has a module that doesn't operate based on domain name
// (ie forward), and a modules that does (ie proxy). It therefore has different module
// when part of the `domains` config, and when not part of the `domains` config the proxy
// modules must have the `domains` property while forward should not have it.
moduleRefs.tcp.push(addDomainRequirement(toSchemaRef('proxy')));
var domainSchema = {
type: 'array'
, items: {
type: 'object'
, properties: {
id: { type: 'string' }
, names: { type: 'array', items: { type: 'string' }, minLength: 1}
, modules: {
type: 'object'
, properties: {
tls: { type: 'array', items: { oneOf: moduleRefs.tls }}
, http: { type: 'array', items: { oneOf: moduleRefs.http }}
, ddns: { type: 'array', items: { oneOf: moduleRefs.ddns }}
, tcp: { type: 'array', items: { oneOf: ['proxy'].map(toSchemaRef)}}
}
, additionalProperties: false
}
}
}
};
var httpSchema = {
type: 'object'
, properties: {
modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.http }) }
// These properties should be snake_case to match the API and config format
, primary_domain: { type: 'string' }
, allow_insecure: { type: 'boolean' }
, trust_proxy: { type: 'boolean' }
// these are forbidden deprecated settings.
, bind: { not: {} }
, domains: { not: {} }
}
};
var tlsSchema = {
type: 'object'
, properties: {
modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.tls }) }
// these are forbidden deprecated settings.
, acme: { not: {} }
, bind: { not: {} }
, domains: { not: {} }
}
};
var tcpSchema = {
type: 'object'
, required: [ 'bind' ]
, properties: {
bind: { type: 'array', items: portSchema, minLength: 1 }
, modules: { type: 'array', items: { oneOf: moduleRefs.tcp }}
}
};
var udpSchema = {
type: 'object'
, properties: {
bind: { type: 'array', items: portSchema }
, modules: { type: 'array', items: { oneOf: moduleRefs.udp }}
}
};
var mdnsSchema = {
type: 'object'
, required: [ 'port', 'broadcast', 'ttl' ]
, properties: {
port: portSchema
, broadcast: { type: 'string' }
, ttl: { type: 'integer', minimum: 0, maximum: 2147483647 }
}
};
var tunnelSvrSchema = {
type: 'object'
, properties: {
servernames: { type: 'array', items: { type: 'string' }}
, secret: { type: 'string' }
}
};
var ddnsSchema = {
type: 'object'
, properties: {
loopback: {
type: 'object'
, required: [ 'type', 'domain' ]
, properties: {
type: { type: 'string', const: 'tunnel@oauth3.org' }
, domain: { type: 'string'}
}
}
, tunnel: {
type: 'object'
, required: [ 'type', 'token_id' ]
, properties: {
type: { type: 'string', const: 'tunnel@oauth3.org' }
, token_id: { type: 'string'}
}
}
, modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.ddns })}
}
};
var socks5Schema = {
type: 'object'
, properties: {
enabled: { type: 'boolean' }
, port: portSchema
}
};
var deviceSchema = {
type: 'object'
, properties: {
hostname: { type: 'string' }
}
};
var mainSchema = {
type: 'object'
, required: [ 'domains', 'http', 'tls', 'tcp', 'udp', 'mdns', 'ddns' ]
, properties: {
domains:domainSchema
, http: httpSchema
, tls: tlsSchema
, tcp: tcpSchema
, udp: udpSchema
, mdns: mdnsSchema
, ddns: ddnsSchema
, socks5: socks5Schema
, device: deviceSchema
, tunnel_server: tunnelSvrSchema
}
, additionalProperties: false
};
function validate(config) {
return validator.validate(recase.snakeCopy(config), mainSchema).errors;
}
module.exports.validate = validate;
class IdList extends Array {
constructor(rawList) {
super();
if (Array.isArray(rawList)) {
Object.assign(this, JSON.parse(JSON.stringify(rawList)));
}
this._itemName = 'item';
}
findId(id) {
return Array.prototype.find.call(this, function (dom) {
return dom.id === id;
});
}
add(item) {
item.id = require('crypto').randomBytes(4).toString('hex');
this.push(item);
}
update(id, update) {
var item = this.findId(id);
if (!item) {
var error = new Error("no "+this._itemName+" with ID '"+id+"'");
error.statusCode = 404;
throw error;
}
Object.assign(this.findId(id), update);
}
remove(id) {
var index = this.findIndex(function (dom) {
return dom.id === id;
});
if (index < 0) {
var error = new Error("no "+this._itemName+" with ID '"+id+"'");
error.statusCode = 404;
throw error;
}
this.splice(index, 1);
}
}
class ModuleList extends IdList {
constructor(rawList) {
super(rawList);
this._itemName = 'module';
}
add(mod) {
if (!mod.type) {
throw new Error("module must have a 'type' defined");
}
if (!moduleSchemas[mod.type]) {
throw new Error("invalid module type '"+mod.type+"'");
}
mod.id = require('crypto').randomBytes(4).toString('hex');
this.push(mod);
}
}
class DomainList extends IdList {
constructor(rawList) {
super(rawList);
this._itemName = 'domain';
this.forEach(function (dom) {
dom.modules = {
http: new ModuleList((dom.modules || {}).http)
, tls: new ModuleList((dom.modules || {}).tls)
, ddns: new ModuleList((dom.modules || {}).ddns)
, tcp: new ModuleList((dom.modules || {}).tcp)
};
});
}
add(dom) {
if (!Array.isArray(dom.names) || !dom.names.length) {
throw new Error("domains must have a non-empty array for 'names'");
}
if (dom.names.some(function (name) { return typeof name !== 'string'; })) {
throw new Error("all domain names must be strings");
}
var modLists = {
http: new ModuleList()
, tls: new ModuleList()
, ddns: new ModuleList()
, tcp: new ModuleList()
};
// We add these after instead of in the constructor to run the validation and manipulation
// in the ModList add function since these are all new modules.
if (dom.modules) {
Object.keys(modLists).forEach(function (key) {
if (Array.isArray(dom.modules[key])) {
dom.modules[key].forEach(modLists[key].add, modLists[key]);
}
});
}
dom.id = require('crypto').randomBytes(4).toString('hex');
dom.modules = modLists;
this.push(dom);
}
}
class ConfigChanger {
constructor(start) {
Object.assign(this, JSON.parse(JSON.stringify(start)));
delete this.device;
delete this.debug;
this.domains = new DomainList(this.domains);
this.http.modules = new ModuleList(this.http.modules);
this.tls.modules = new ModuleList(this.tls.modules);
this.tcp.modules = new ModuleList(this.tcp.modules);
this.udp.modules = new ModuleList(this.udp.modules);
this.ddns.modules = new ModuleList(this.ddns.modules);
}
update(update) {
var self = this;
if (update.domains) {
update.domains.forEach(self.domains.add, self.domains);
}
[ 'http', 'tls', 'tcp', 'udp', 'ddns' ].forEach(function (name) {
if (update[name] && update[name].modules) {
update[name].modules.forEach(self[name].modules.add, self[name].modules);
delete update[name].modules;
}
});
function mergeSettings(orig, changes) {
Object.keys(changes).forEach(function (key) {
// TODO: use an API that can properly handle updating arrays.
if (!changes[key] || (typeof changes[key] !== 'object') || Array.isArray(changes[key])) {
orig[key] = changes[key];
}
else if (!orig[key] || typeof orig[key] !== 'object') {
orig[key] = changes[key];
}
else {
mergeSettings(orig[key], changes[key]);
}
});
}
mergeSettings(this, update);
return validate(this);
}
validate() {
return validate(this);
}
}
module.exports.ConfigChanger = ConfigChanger;

31
lib/admin/index.js Normal file
View File

@ -0,0 +1,31 @@
var adminDomains = [
'localhost.alpha.daplie.me'
, 'localhost.admin.daplie.me'
, 'alpha.localhost.daplie.me'
, 'admin.localhost.daplie.me'
, 'localhost.daplie.invalid'
];
module.exports.adminDomains = adminDomains;
module.exports.create = function (deps, conf) {
'use strict';
var path = require('path');
var express = require('express');
var app = express();
var apis = require('./apis').create(deps, conf);
app.use('/api/goldilocks@daplie.com', apis);
app.use('/api/com.daplie.goldilocks', apis);
// Serve the static assets for the UI (even though it probably won't be used very
// often since it only works on localhost domains). Note that we are using the default
// .well-known directory from the oauth3 library even though it indicates we have
// capabilities we don't support because it's simpler and it's unlikely anything will
// actually use it to determine our API (it is needed to log into the web page).
app.use('/.well-known', express.static(path.join(__dirname, '../../packages/assets/well-known')));
app.use('/assets', express.static(path.join(__dirname, '../../packages/assets')));
app.use('/', express.static(path.join(__dirname, '../../admin/public')));
return require('http').createServer(app);
};

View File

@ -1,325 +0,0 @@
'use strict';
module.exports = function (myDeps, conf, overrideHttp) {
var express = require('express');
//var finalhandler = require('finalhandler');
var serveStatic = require('serve-static');
var serveIndex = require('serve-index');
//var assetServer = serveStatic(opts.assetsPath);
var path = require('path');
//var wellKnownServer = serveStatic(path.join(opts.assetsPath, 'well-known'));
var serveStaticMap = {};
var serveIndexMap = {};
var content = conf.content;
//var server;
var goldilocksApis;
var app;
var request;
function createGoldilocksApis() {
var PromiseA = require('bluebird');
var OAUTH3 = require('../packages/assets/org.oauth3');
require('../packages/assets/org.oauth3/oauth3.domains.js');
require('../packages/assets/org.oauth3/oauth3.dns.js');
require('../packages/assets/org.oauth3/oauth3.tunnel.js');
OAUTH3._hooks = require('../packages/assets/org.oauth3/oauth3.node.storage.js');
request = request || PromiseA.promisify(require('request'));
myDeps.PromiseA = PromiseA;
myDeps.OAUTH3 = OAUTH3;
myDeps.recase = require('recase').create({});
myDeps.request = request;
return require('../packages/apis/com.daplie.goldilocks').create(myDeps, conf);
}
app = express();
var Sites = {
add: function (sitesMap, site) {
if (!sitesMap[site.$id]) {
sitesMap[site.$id] = site;
}
if (!site.paths) {
site.paths = [];
}
if (!site.paths._map) {
site.paths._map = {};
}
site.paths.forEach(function (path) {
site.paths._map[path.$id] = path;
if (!path.modules) {
path.modules = [];
}
if (!path.modules._map) {
path.modules._map = {};
}
path.modules.forEach(function (module) {
path.modules._map[module.$id] = module;
});
});
}
};
var opts = overrideHttp || conf.http;
if (!opts.defaults) {
opts.defaults = {};
}
if (!opts.global) {
opts.global = {};
}
if (!opts.sites) {
opts.sites = [];
}
opts.sites._map = {};
opts.sites.forEach(function (site) {
Sites.add(opts.sites._map, site);
});
function mapMap(el, i, arr) {
arr._map[el.$id] = el;
}
opts.global.modules._map = {};
opts.global.modules.forEach(mapMap);
opts.global.paths._map = {};
opts.global.paths.forEach(function (path, i, arr) {
mapMap(path, i, arr);
//opts.global.paths._map[path.$id] = path;
path.modules._map = {};
path.modules.forEach(mapMap);
});
opts.sites.forEach(function (site) {
site.paths._map = {};
site.paths.forEach(function (path, i, arr) {
mapMap(path, i, arr);
//site.paths._map[path.$id] = path;
path.modules._map = {};
path.modules.forEach(mapMap);
});
});
opts.defaults.modules._map = {};
opts.defaults.modules.forEach(mapMap);
opts.defaults.paths._map = {};
opts.defaults.paths.forEach(function (path, i, arr) {
mapMap(path, i, arr);
//opts.global.paths._map[path.$id] = path;
path.modules._map = {};
path.modules.forEach(mapMap);
});
function _goldApis(req, res, next) {
if (!goldilocksApis) {
goldilocksApis = createGoldilocksApis();
}
if (typeof goldilocksApis[req.params.name] === 'function') {
goldilocksApis[req.params.name](req, res);
} else {
next();
}
}
return app
.use('/api/com.daplie.goldilocks/:name', _goldApis)
.use('/api/goldilocks@daplie.com/:name', _goldApis)
.use('/', function (req, res, next) {
if (!req.headers.host) {
next(new Error('missing HTTP Host header'));
return;
}
if (content && '/' === req.url) {
// res.setHeader('Content-Type', 'application/octet-stream');
res.end(content);
return;
}
//var done = finalhandler(req, res);
var host = req.headers.host;
var hostname = (host||'').split(':')[0].toLowerCase();
console.log('opts.global', opts.global);
var sites = [ opts.global || null, opts.sites._map[hostname] || null, opts.defaults || null ];
var loadables = {
serve: function (config, hostname, pathname, req, res, next) {
var originalUrl = req.url;
var dirpaths = config.paths.slice(0);
function nextServe() {
var dirname = dirpaths.pop();
if (!dirname) {
req.url = originalUrl;
next();
return;
}
console.log('[serve]', req.url, hostname, pathname, dirname);
dirname = path.resolve(conf.cwd, dirname.replace(/:hostname/, hostname));
if (!serveStaticMap[dirname]) {
serveStaticMap[dirname] = serveStatic(dirname);
}
serveStaticMap[dirname](req, res, nextServe);
}
req.url = req.url.substr(pathname.length - 1);
nextServe();
}
, indexes: function (config, hostname, pathname, req, res, next) {
var originalUrl = req.url;
var dirpaths = config.paths.slice(0);
function nextIndex() {
var dirname = dirpaths.pop();
if (!dirname) {
req.url = originalUrl;
next();
return;
}
console.log('[indexes]', req.url, hostname, pathname, dirname);
dirname = path.resolve(conf.cwd, dirname.replace(/:hostname/, hostname));
if (!serveStaticMap[dirname]) {
serveIndexMap[dirname] = serveIndex(dirname);
}
serveIndexMap[dirname](req, res, nextIndex);
}
req.url = req.url.substr(pathname.length - 1);
nextIndex();
}
, app: function (config, hostname, pathname, req, res, next) {
//var appfile = path.resolve(/*process.cwd(), */config.path.replace(/:hostname/, hostname));
var appfile = config.path.replace(/:hostname/, hostname);
try {
var app = require(appfile);
app(req, res, next);
} catch (err) {
next();
}
}
};
function runModule(module, hostname, pathname, modulename, req, res, next) {
if (!loadables[modulename]) {
next(new Error("no module '" + modulename + "' found"));
return;
}
loadables[modulename](module, hostname, pathname, req, res, next);
}
function iterModules(modules, hostname, pathname, req, res, next) {
console.log('modules');
console.log(modules);
var modulenames = Object.keys(modules._map);
function nextModule() {
var modulename = modulenames.pop();
if (!modulename) {
next();
return;
}
console.log('modules', modules);
runModule(modules._map[modulename], hostname, pathname, modulename, req, res, nextModule);
}
nextModule();
}
function iterPaths(site, hostname, req, res, next) {
console.log('site', hostname);
console.log(site);
var pathnames = Object.keys(site.paths._map);
console.log('pathnames', pathnames);
pathnames = pathnames.filter(function (pathname) {
// TODO ensure that pathname has trailing /
return (0 === req.url.indexOf(pathname));
//return req.url.match(pathname);
});
pathnames.sort(function (a, b) {
return b.length - a.length;
});
console.log('pathnames', pathnames);
function nextPath() {
var pathname = pathnames.shift();
if (!pathname) {
next();
return;
}
console.log('iterPaths', hostname, pathname, req.url);
iterModules(site.paths._map[pathname].modules, hostname, pathname, req, res, nextPath);
}
nextPath();
}
function nextSite() {
console.log('hostname', hostname, sites);
var site;
if (!sites.length) {
next(); // 404
return;
}
site = sites.shift();
if (!site) {
nextSite();
return;
}
iterPaths(site, hostname, req, res, nextSite);
}
nextSite();
/*
function serveStaticly(server) {
function serveTheStatic() {
server.serve(req, res, function (err) {
if (err) { return done(err); }
server.index(req, res, function (err) {
if (err) { return done(err); }
req.url = req.url.replace(/\/assets/, '');
assetServer(req, res, function () {
if (err) { return done(err); }
req.url = req.url.replace(/\/\.well-known/, '');
wellKnownServer(req, res, done);
});
});
});
}
if (server.expressApp) {
server.expressApp(req, res, serveTheStatic);
return;
}
serveTheStatic();
}
if (opts.livereload) {
res.__my_livereload = '<script src="//'
+ (host || opts.sites[0].name).split(':')[0]
+ ':35729/livereload.js?snipver=1"></script>';
res.__my_addLen = res.__my_livereload.length;
// TODO modify prototype instead of each instance?
res.__write = res.write;
res.write = _reloadWrite;
}
console.log('hostname:', hostname, opts.sites[0].paths);
addServer(hostname);
server = hostsMap[hostname] || hostsMap[opts.sites[0].name];
serveStaticly(server);
*/
});
};

View File

@ -1,149 +0,0 @@
'use strict';
module.exports.create = function (deps, conf) {
var PromiseA = deps.PromiseA;
var request = PromiseA.promisify(require('request'));
var OAUTH3 = require('../packages/assets/org.oauth3');
require('../packages/assets/org.oauth3/oauth3.dns.js');
OAUTH3._hooks = require('../packages/assets/org.oauth3/oauth3.node.storage.js');
function dnsType(addr) {
if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) {
return 'A';
}
if (-1 !== addr.indexOf(':') && /^[a-f:\.\d]+$/i.test(addr)) {
return 'AAAA';
}
}
async function getSession() {
var sessions = await deps.storage.owners.all();
var session = sessions.filter(function (sess) {
return sess.token.scp.indexOf('dns') >= 0;
})[0];
if (!session) {
throw new Error('no sessions with DNS grants');
}
// The OAUTH3 library stores some things on the root session object that we usually
// just leave inside the token, but we need to pull those out before we use it here
session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss;
session.client_uri = session.client_uri || session.token.azp;
session.scope = session.scope || session.token.scp;
return session;
}
async function setDeviceAddress(addr) {
var session = await getSession();
var directives = await OAUTH3.discover(session.token.aud);
// Set the address of the device to our public address.
await request({
url: directives.api+'/api/com.daplie.domains/acl/devices/' + conf.device.hostname
, method: 'POST'
, headers: {
'Authorization': 'Bearer ' + session.refresh_token
, 'Accept': 'application/json; charset=utf-8'
}
, json: {
addresses: [
{ value: addr, type: dnsType(addr) }
]
}
});
// Then update all of the records attached to our hostname, first removing the old records
// to remove the reference to the old address, then creating new records for the same domains
// using our new address.
var allDns = OAUTH3.api(directives.api, {session: session, api: 'dns.list'});
var ourDomains = allDns.filter(function (record) {
return record.device === conf.device.hostname;
}).map(function (record) {
var zoneSplit = record.zone.split('.');
return {
tld: zoneSplit.slice(1).join('.')
, sld: zoneSplit[0]
, sub: record.host.slice(0, -(record.zone.length + 1))
};
});
var common = {
api: 'devices.detach'
, session: session
, device: conf.device.hostname
};
await PromiseA.all(ourDomains.map(function (record) {
return OAUTH3.api(directives.api, Object.assign({}, common, record));
}));
common = {
api: 'devices.attach'
, session: session
, device: conf.device.hostname
, ip: addr
, ttl: 300
};
await PromiseA.all(ourDomains.map(function (record) {
return OAUTH3.api(directives.api, Object.assign({}, common, record));
}));
}
async function getDeviceAddresses() {
var session = await getSession();
var directives = await OAUTH3.discover(session.token.aud);
var result = await request({
url: directives.api+'/api/org.oauth3.dns/acl/devices'
, method: 'GET'
, headers: {
'Authorization': 'Bearer ' + session.refresh_token
, 'Accept': 'application/json; charset=utf-8'
}
, json: true
});
if (!result.body) {
throw new Error('No response body in request for device addresses');
}
if (result.body.error) {
throw Object.assign(new Error('error getting device list'), result.body.error);
}
var dev = result.body.devices.filter(function (dev) {
return dev.name === conf.device.hostname;
})[0];
return (dev || {}).addresses || [];
}
var publicAddress;
async function recheckPubAddr() {
if (!conf.ddns.enabled) {
return;
}
var session = await getSession();
var directives = await OAUTH3.discover(session.token.aud);
var addr = await deps.loopback.checkPublicAddr(directives.api);
if (publicAddress === addr) {
return;
}
if (conf.debug) {
console.log('previous public address',publicAddress, 'does not match current public address', addr);
}
await setDeviceAddress(addr);
publicAddress = addr;
}
recheckPubAddr();
setInterval(recheckPubAddr, 5*60*1000);
return {
setDeviceAddress: setDeviceAddress
, getDeviceAddresses: getDeviceAddresses
, recheckPubAddr: recheckPubAddr
};
};

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
};
};

132
lib/ddns/dns-ctrl.js Normal file
View File

@ -0,0 +1,132 @@
'use strict';
module.exports.create = function (deps, conf, utils) {
function dnsType(addr) {
if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) {
return 'A';
}
if (-1 !== addr.indexOf(':') && /^[a-f:\.\d]+$/i.test(addr)) {
return 'AAAA';
}
}
async function setDeviceAddress(session, addr, domains) {
var directives = await deps.OAUTH3.discover(session.token.aud);
// Set the address of the device to our public address.
await deps.request({
url: deps.OAUTH3.url.normalize(directives.api)+'/api/com.daplie.domains/acl/devices/' + conf.device.hostname
, method: 'POST'
, headers: {
'Authorization': 'Bearer ' + session.refresh_token
, 'Accept': 'application/json; charset=utf-8'
}
, json: {
addresses: [
{ value: addr, type: dnsType(addr) }
]
}
});
// Then update all of the records attached to our hostname, first removing the old records
// to remove the reference to the old address, then creating new records for the same domains
// using our new address.
var allDns = await deps.OAUTH3.api(directives.api, {session: session, api: 'dns.list'});
var ourDns = allDns.filter(function (record) {
if (record.device !== conf.device.hostname) {
return false;
}
if ([ 'A', 'AAAA' ].indexOf(record.type) < 0) {
return false;
}
return domains.indexOf(record.host) !== -1;
});
// Of all the DNS records referring to our device and the current list of domains determine
// which domains have records with outdated address, and which ones we can just leave be
// without updating them.
var badAddrDomains = ourDns.filter(function (record) {
return record.value !== addr;
}).map(record => record.host);
var goodAddrDomains = ourDns.filter(function (record) {
return record.value === addr && badAddrDomains.indexOf(record.host) < 0;
}).map(record => record.host);
var requiredUpdates = domains.filter(function (domain) {
return goodAddrDomains.indexOf(domain) < 0;
});
var oldDns = await utils.splitDomains(directives.api, badAddrDomains);
var common = {
api: 'devices.detach'
, session: session
, device: conf.device.hostname
};
await deps.PromiseA.all(oldDns.map(function (record) {
return deps.OAUTH3.api(directives.api, Object.assign({}, common, record));
}));
if (conf.debug && badAddrDomains.length) {
console.log('removed bad DNS records for ' + badAddrDomains.join(', '));
}
var newDns = await utils.splitDomains(directives.api, requiredUpdates);
common = {
api: 'devices.attach'
, session: session
, device: conf.device.hostname
, ip: addr
, ttl: 300
};
await deps.PromiseA.all(newDns.map(function (record) {
return deps.OAUTH3.api(directives.api, Object.assign({}, common, record));
}));
if (conf.debug && requiredUpdates.length) {
console.log('set new DNS records for ' + requiredUpdates.join(', '));
}
}
async function getDeviceAddresses(session) {
var directives = await deps.OAUTH3.discover(session.token.aud);
var result = await deps.request({
url: deps.OAUTH3.url.normalize(directives.api)+'/api/org.oauth3.dns/acl/devices'
, method: 'GET'
, headers: {
'Authorization': 'Bearer ' + session.refresh_token
, 'Accept': 'application/json; charset=utf-8'
}
, json: true
});
if (!result.body) {
throw new Error('No response body in request for device addresses');
}
if (result.body.error) {
throw Object.assign(new Error('error getting device list'), result.body.error);
}
var dev = result.body.devices.filter(function (dev) {
return dev.name === conf.device.hostname;
})[0];
return (dev || {}).addresses || [];
}
async function removeDomains(session, domains) {
var directives = await deps.OAUTH3.discover(session.token.aud);
var oldDns = await utils.splitDomains(directives.api, domains);
var common = {
api: 'devices.detach'
, session: session
, device: conf.device.hostname
};
await deps.PromiseA.all(oldDns.map(function (record) {
return deps.OAUTH3.api(directives.api, Object.assign({}, common, record));
}));
}
return {
getDeviceAddresses
, setDeviceAddress
, removeDomains
};
};

326
lib/ddns/index.js Normal file
View File

@ -0,0 +1,326 @@
'use strict';
module.exports.create = function (deps, conf) {
var dns = deps.PromiseA.promisifyAll(require('dns'));
var network = deps.PromiseA.promisifyAll(deps.recase.camelCopy(require('network')));
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 tunnelActive = false;
async function startTunnel(tunnelSession, mod, domainList) {
try {
var dnsSession = await utils.getSession(mod.tokenId);
var tunnelDomain = await tunnelClients.start(tunnelSession || dnsSession, domainList);
var addrList;
try {
addrList = await dns.resolve4Async(tunnelDomain);
} catch (e) {}
if (!addrList || !addrList.length) {
try {
addrList = await dns.resolve6Async(tunnelDomain);
} catch (e) {}
}
if (!addrList || !addrList.length || !addrList[0]) {
throw new Error('failed to lookup IP for tunnel domain "' + tunnelDomain + '"');
}
if (!mod.disabled) {
await dnsCtrl.setDeviceAddress(dnsSession, addrList[0], domainList);
}
} catch (err) {
console.log('error starting tunnel for', domainList.join(', '));
console.log(err);
}
}
async function connectAllTunnels() {
var tunnelSession;
if (conf.ddns.tunnel) {
// In the case of a non-existant token, I'm not sure if we want to throw here and prevent
// any tunnel connections, or if we want to ignore it and fall back to the DNS tokens
tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId);
}
await utils.iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return null; }
return startTunnel(tunnelSession, mod, domainList);
});
tunnelActive = true;
}
async function disconnectTunnels() {
tunnelClients.disconnect();
tunnelActive = false;
await Promise.resolve();
}
async function checkTunnelTokens() {
var oldTokens = tunnelClients.current();
var newTokens = await utils.iterateAllModules(function checkTokens(mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return null; }
var domainStr = domainList.slice().sort().join(',');
// If there is already a token handling exactly the domains this modules
// needs handled remove it from the list of tokens to be removed. Otherwise
// return the module and domain list so we can get new tokens.
if (oldTokens[domainStr]) {
delete oldTokens[domainStr];
} else {
return Promise.resolve({ mod, domainList });
}
});
await Promise.all(Object.values(oldTokens).map(tunnelClients.remove));
if (!newTokens.length) { return; }
var tunnelSession;
if (conf.ddns.tunnel) {
// In the case of a non-existant token, I'm not sure if we want to throw here and prevent
// any tunnel connections, or if we want to ignore it and fall back to the DNS tokens
tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId);
}
await Promise.all(newTokens.map(function ({mod, domainList}) {
return startTunnel(tunnelSession, mod, domainList);
}));
}
var localAddr, gateway;
async function checkNetworkEnv() {
// Since we can't detect the OS level events when a user plugs in an ethernet cable to recheck
// what network environment we are in we check our local network address and the gateway to
// determine if we need to run the loopback check and router configuration again.
var addr = await network.getPrivateIpAsync();
// Until the author of the `network` package publishes the pull request we gave him
// checking the gateway on our units fails because we have the busybox versions of
// the linux commands. Gateway is realistically less important than address, so if
// we fail in getting it go ahead and use the null value.
var gw;
try {
gw = await network.getGatewayIpAsync();
} catch (err) {
gw = null;
}
if (localAddr === addr && gateway === gw) {
return;
}
var loopResult = await loopback(loopbackDomain);
var notLooped = Object.keys(loopResult.ports).filter(function (port) {
return !loopResult.ports[port];
});
// if (notLooped.length) {
// // TODO: try to automatically configure router to forward ports to us.
// }
// If we are on a public address or all ports we are listening on are forwarded to us then
// we don't need the tunnel and we can set the DNS records for all our domains to our public
// address. Otherwise we need to use the tunnel to accept traffic. Also since the tunnel will
// only be listening on ports 80 and 443 if those are forwarded to us we don't want the tunnel.
if (!notLooped.length || (loopResult.ports['80'] && loopResult.ports['443'])) {
if (tunnelActive) {
await disconnectTunnels();
}
} else {
if (!tunnelActive) {
await connectAllTunnels();
}
}
// Don't assign these until the end of the function. This means that if something failed
// in the loopback or tunnel connection that we will try to go through the whole process
// again next time and hopefully the error is temporary (but if not I'm not sure what the
// correct course of action would be anyway).
localAddr = addr;
gateway = gw;
}
var publicAddress;
async function recheckPubAddr() {
await checkNetworkEnv();
if (tunnelActive) {
return;
}
var addr = await loopback.checkPublicAddr(loopbackDomain);
if (publicAddress === addr) {
return;
}
if (conf.debug) {
console.log('previous public address',publicAddress, 'does not match current public address', addr);
}
publicAddress = addr;
await utils.iterateAllModules(function setModuleDNS(mod, domainList) {
if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; }
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(', '));
console.log(err);
});
});
}
function getModuleDiffs(prevConf) {
var prevMods = {};
var curMods = {};
// 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.
utils.iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return; }
prevMods[mod.id] = { mod, domainList };
return true;
}, prevConf);
utils.iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return; }
curMods[mod.id] = { mod, domainList };
return true;
});
// Filter out all of the modules that are exactly the same including domainList
// since there is no required action to transition.
Object.keys(prevMods).map(function (id) {
if (equal(prevMods[id], curMods[id])) {
delete prevMods[id];
delete curMods[id];
}
});
return {prevMods, curMods};
}
async function cleanOldDns(prevConf) {
var {prevMods, curMods} = getModuleDiffs(prevConf);
// 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] || curMods[mod.id].disabled || mod.tokenId !== curMods[mod.id].mod.tokenId) {
oldDomains = domainList.slice();
} else {
oldDomains = domainList.filter(function (domain) {
return curMods[mod.id].domainList.indexOf(domain) < 0;
});
}
if (conf.debug) {
console.log('removing old domains for module', mod.id, oldDomains.join(', '));
}
if (!oldDomains.length) {
return;
}
return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.removeDomains(session, oldDomains);
});
}).filter(Boolean));
}
async function setNewDns(prevConf) {
var {prevMods, curMods} = getModuleDiffs(prevConf);
// 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();
} else {
newDomains = domainList.filter(function (domain) {
return prevMods[mod.id].domainList.indexOf(domain) < 0;
});
}
if (conf.debug) {
console.log('adding new domains for module', mod.id, newDomains.join(', '));
}
if (!newDomains.length) {
return;
}
return utils.getSession(mod.tokenId).then(function (session) {
return dnsCtrl.setDeviceAddress(session, publicAddress, newDomains);
});
}).filter(Boolean));
}
function check() {
recheckPubAddr().catch(function (err) {
console.error('failed to handle all actions needed for DDNS');
console.error(err);
});
}
check();
setInterval(check, 5*60*1000);
var curConf;
function updateConf() {
if (curConf && equal(curConf.ddns, conf.ddns) && equal(curConf.domains, conf.domains)) {
// We could update curConf, but since everything we care about is the same...
return;
}
if (!curConf || !equal(curConf.ddns.loopback, conf.ddns.loopback)) {
loopbackDomain = 'oauth3.org';
if (conf.ddns && conf.ddns.loopback) {
if (conf.ddns.loopback.type === 'tunnel@oauth3.org' && conf.ddns.loopback.domain) {
loopbackDomain = conf.ddns.loopback.domain;
} else {
console.error('invalid loopback configuration: bad type or missing domain');
}
}
}
if (!curConf) {
// We need to make a deep copy of the config so we can use it next time to
// compare and see what setup/cleanup is needed to adapt to the changes.
curConf = JSON.parse(JSON.stringify(conf));
return;
}
cleanOldDns(curConf).then(function () {
if (!tunnelActive) {
return setNewDns(curConf);
}
if (equal(curConf.ddns.tunnel, conf.ddns.tunnel)) {
return checkTunnelTokens();
} else {
return disconnectTunnels().then(connectAllTunnels);
}
}).catch(function (err) {
console.error('error transitioning DNS between configurations');
console.error(err);
}).then(function () {
// We need to make a deep copy of the config so we can use it next time to
// compare and see what setup/cleanup is needed to adapt to the changes.
curConf = JSON.parse(JSON.stringify(conf));
});
}
updateConf();
return {
loopbackServer: loopback.server
, setDeviceAddress: dnsCtrl.setDeviceAddress
, getDeviceAddresses: dnsCtrl.getDeviceAddresses
, recheckPubAddr: recheckPubAddr
, updateConf: updateConf
, challenge
};
};

View File

@ -1,14 +1,12 @@
'use strict';
module.exports.create = function (deps, conf) {
var PromiseA = require('bluebird');
var request = PromiseA.promisify(require('request'));
var pending = {};
async function checkPublicAddr(host) {
var result = await request({
async function _checkPublicAddr(host) {
var result = await deps.request({
method: 'GET'
, url: host+'/api/org.oauth3.tunnel/checkip'
, url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/checkip'
, json: true
});
@ -19,8 +17,15 @@ module.exports.create = function (deps, conf) {
// Note that the error on the body will probably have a message that overwrites the default
throw Object.assign(new Error('error in check IP response'), result.body.error);
}
if (!result.body.address) {
throw new Error("public address resonse doesn't contain address: "+JSON.stringify(result.body));
}
return result.body.address;
}
async function checkPublicAddr(provider) {
var directives = await deps.OAUTH3.discover(provider);
return _checkPublicAddr(directives.api);
}
async function checkSinglePort(host, address, port) {
var crypto = require('crypto');
@ -30,27 +35,35 @@ module.exports.create = function (deps, conf) {
var reqObj = {
method: 'POST'
, url: host+'/api/org.oauth3.tunnel/loopback'
, url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/loopback'
, timeout: 20*1000
, json: {
address: address
, port: port
, token: token
, keyAuthorization: keyAuth
, iat: Date.now()
, timeout: 18*1000
}
};
var result;
try {
result = await request(reqObj);
result = await deps.request(reqObj);
} catch (err) {
delete pending[token];
throw err;
if (conf.debug) {
console.log('error making loopback request for port ' + port + ' loopback', err.message);
}
return false;
}
delete pending[token];
if (!result.body) {
throw new Error('No response body in loopback request for port '+port);
if (conf.debug) {
console.log('No response body in loopback request for port '+port);
}
return false;
}
// If the loopback requests don't go to us then there are all kinds of ways it could
// error, but none of them really provide much extra information so we don't do
@ -63,23 +76,27 @@ module.exports.create = function (deps, conf) {
async function loopback(provider) {
var directives = await deps.OAUTH3.discover(provider);
var address = await checkPublicAddr(directives.api);
var address = await _checkPublicAddr(directives.api);
if (conf.debug) {
console.log('checking to see if', address, 'gets back to us');
}
var ports = require('./servers').listeners.tcp.list();
var values = await PromiseA.all(ports.map(function (port) {
var ports = require('../servers').listeners.tcp.list();
var values = await deps.PromiseA.all(ports.map(function (port) {
return checkSinglePort(directives.api, address, port);
}));
if (conf.debug) {
if (conf.debug && Object.keys(pending).length) {
console.log('remaining loopback tokens', pending);
}
var result = {error: null, address: address};
ports.forEach(function (port, ind) {
result[port] = values[ind];
});
return result;
return {
address: address
, ports: ports.reduce(function (obj, port, ind) {
obj[port] = values[ind];
return obj;
}, {})
};
}
loopback.checkPublicAddr = checkPublicAddr;

View File

@ -0,0 +1,191 @@
'use strict';
module.exports.create = function (deps, config) {
var stunnel = require('stunnel');
var jwt = require('jsonwebtoken');
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 };
}
if (!data.jwt) {
throw new Error("missing 'jwt' from tunnel data");
}
var decoded = jwt.decode(data.jwt);
if (!decoded) {
throw new Error('invalid JWT');
}
if (!data.tunnelUrl) {
if (!decoded.aud) {
throw new Error('missing tunnelUrl and audience');
}
data.tunnelUrl = 'wss://' + decoded.aud + '/';
}
data.domains = (decoded.domains || []).slice().sort().join(',');
if (!data.domains) {
throw new Error('JWT contains no domains to be forwarded');
}
return data;
}
async function removeToken(data) {
data = fillData(data);
// Not sure if we might want to throw an error indicating the token didn't
// even belong to a server that existed, but since it never existed we can
// consider it as "removed".
if (!activeTunnels[data.tunnelUrl]) {
return;
}
console.log('removing token from tunnel at', data.tunnelUrl);
return activeTunnels[data.tunnelUrl].clear(data.jwt).then(function () {
delete activeDomains[data.domains];
});
}
async function addToken(data) {
data = fillData(data);
if (activeDomains[data.domains]) {
// If already have a token with the exact same domains and to the same tunnel
// server there isn't really a need to add a new one
if (activeDomains[data.domains].tunnelUrl === data.tunnelUrl) {
return;
}
// Otherwise we want to detach from the other tunnel server in favor of the new one
console.warn('added token with the exact same domains as another');
await removeToken(activeDomains[data.domains]);
}
if (!activeTunnels[data.tunnelUrl]) {
console.log('creating new tunnel client for', data.tunnelUrl);
// We create the tunnel without an initial token so we can append the token and
// get the promise that should tell us more about if it worked or not.
activeTunnels[data.tunnelUrl] = stunnel.connect({
stunneld: data.tunnelUrl
, 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
// a '*' sub-property.
, services: {
https: { '*': 443 }
, http: { '*': 80 }
, smtp: { '*': 25 }
, smtps: { '*': 587 /*also 465/starttls*/ }
, ssh: { '*': 22 }
}
});
}
console.log('appending token to tunnel at', data.tunnelUrl, 'for domains', data.domains);
await activeTunnels[data.tunnelUrl].append(data.jwt);
// Now that we know the tunnel server accepted our token we can save it
// to keep record of what domains we are handling and what tunnel server
// those domains should go to.
activeDomains[data.domains] = data;
// This is mostly for the start, but return the host for the tunnel server
// we've connected to (after stripping the protocol and path away).
return data.tunnelUrl.replace(/^[a-z]*:\/\//i, '').replace(/\/.*/, '');
}
async function acquireToken(session, domains) {
var OAUTH3 = deps.OAUTH3;
// The OAUTH3 library stores some things on the root session object that we usually
// just leave inside the token, but we need to pull those out before we use it here
session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss;
session.client_uri = session.client_uri || session.token.azp;
session.scope = session.scope || session.token.scp;
console.log('asking for tunnel token from', session.token.aud);
var opts = {
api: 'tunnel.token'
, session: session
, data: {
domains: domains
, device: {
hostname: config.device.hostname
, id: config.device.uid || config.device.id
}
}
};
var directives = await OAUTH3.discover(session.token.aud);
var tokenData = await OAUTH3.api(directives.api, opts);
return addToken(tokenData);
}
function disconnectAll() {
Object.keys(activeTunnels).forEach(function (key) {
activeTunnels[key].end();
});
}
function currentTokens() {
return JSON.parse(JSON.stringify(activeDomains));
}
return {
start: acquireToken
, startDirect: addToken
, remove: removeToken
, disconnect: disconnectAll
, current: currentTokens
};
};

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,258 +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 modules;
function loadModules() {
modules = {};
modules.tls = require('./modules/tls').create(deps, config, netHandler);
modules.http = require('./modules/http.js').create(deps, config, modules.tls.middleware);
}
// 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 netHandler(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('[netHandler]', logName, 'encrypted: '+opts.encrypted);
var start = Date.now();
conn.on('timeout', function () {
console.log('[netHandler]', logName, 'connection timed out', (Date.now()-start)/1000);
});
conn.on('end', function () {
console.log('[netHandler]', logName, 'connection ended', (Date.now()-start)/1000);
});
conn.on('close', function () {
console.log('[netHandler]', logName, 'connection closed', (Date.now()-start)/1000);
});
// 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 dnsListener(msg) {
if (!Array.isArray(config.dns.modules)) {
return;
}
var socket = require('dgram').createSocket('udp4');
config.dns.modules.forEach(function (mod) {
if (mod.name !== 'proxy') {
console.warn('found bad DNS module', mod);
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 = {};
['remote', 'local'].forEach(function (end) {
['Family', 'Address', 'Port'].forEach(function (name) {
newConnOpts['_'+end+name] = conn[end+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) {
}
netHandler(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 = {};
function addPorts(bindList) {
if (!bindList) {
return;
}
if (Array.isArray(bindList)) {
bindList.filter(Number).forEach(function (port) {
tcpPortMap[port] = true;
});
}
else if (Number(bindList)) {
tcpPortMap[bindList] = true;
}
}
addPorts(config.tcp.bind);
(config.tcp.modules || []).forEach(function (mod) {
if (mod.name === '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 {
console.warn('unknown TCP module specified', mod);
}
});
// Even though these ports were specified in different places we treat any TCP
// connections we haven't been told to just forward exactly as is equal so that
// we can potentially use the same ports for different protocols.
addPorts(config.tls.bind);
addPorts(config.http.bind);
var portList = Object.keys(tcpPortMap).map(Number).sort();
portList.forEach(function (port) {
listenPromises.push(listeners.tcp.add(port, netHandler));
});
if (config.dns.bind) {
if (Array.isArray(config.dns.bind)) {
config.dns.bind.map(function (port) {
listenPromises.push(listeners.udp.add(port, dnsListener));
});
} else {
listenPromises.push(listeners.udp.add(config.dns.bind, dnsListener));
}
}
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,7 +136,14 @@ module.exports.start = function (deps, config, mainPort) {
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 () {
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
// 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

@ -1,67 +0,0 @@
var adminDomains = [
'localhost.alpha.daplie.me'
, 'localhost.admin.daplie.me'
, 'alpha.localhost.daplie.me'
, 'admin.localhost.daplie.me'
, 'localhost.daplie.invalid'
];
module.exports.adminDomains = adminDomains;
module.exports.create = function (deps, conf) {
'use strict';
var path = require('path');
//var defaultServername = 'localhost.daplie.me';
//var defaultWebRoot = '.';
var assetsPath = path.join(__dirname, '..', '..', 'packages', 'assets');
var opts = {};
opts.global = opts.global || {};
opts.sites = opts.sites || [];
opts.sites._map = {};
// argv.sites
opts.groups = [];
// 'packages', 'assets', 'com.daplie.goldilocks'
opts.global = {
modules: [ // TODO uh-oh we've got a mixed bag of modules (various types), a true map
{ $id: 'greenlock', email: opts.email, tos: opts.tos }
, { $id: 'rvpn', email: opts.email, tos: opts.tos }
//, { $id: 'content', content: content }
, { $id: 'livereload', on: opts.livereload }
, { $id: 'app', path: opts.expressApp }
]
, paths: [
{ $id: '/assets/', modules: [ { $id: 'serve', paths: [ assetsPath ] } ] }
// TODO figure this b out
, { $id: '/.well-known/', modules: [
{ $id: 'serve', paths: [ path.join(assetsPath, 'well-known') ] }
] }
]
};
opts.defaults = {
modules: []
, paths: [
/*
{ $id: '/', modules: [
{ $id: 'serve', paths: [ defaultWebRoot ] }
, { $id: 'indexes', paths: [ defaultWebRoot ] }
] }
*/
]
};
adminDomains.forEach(function (id) {
opts.sites.push({
$id: id
, paths: [
{ $id: '/', modules: [ { $id: 'serve', paths: [ path.resolve(__dirname, '..', '..', 'admin', 'public') ] } ] }
, { $id: '/api/', modules: [ { $id: 'app', path: path.join(__dirname, 'admin') } ] }
]
});
});
var app = require('../app.js')(deps, conf, opts);
return require('http').createServer(app);
};

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) {
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

@ -3,6 +3,8 @@
var PromiseA = require('bluebird');
var path = require('path');
var fs = PromiseA.promisifyAll(require('fs'));
var jwt = require('jsonwebtoken');
var crypto = require('crypto');
module.exports.create = function (deps, conf) {
var hrIds = require('human-readable-ids').humanReadableIds;
@ -65,12 +67,129 @@ module.exports.create = function (deps, conf) {
}
};
var confCb;
var config = {
save: function (changes) {
deps.messenger.send({
type: 'com.daplie.goldilocks/config'
, changes: changes
});
return new deps.PromiseA(function (resolve, reject) {
var timeoutId = setTimeout(function () {
reject(new Error('Did not receive config update from main process in a reasonable time'));
confCb = null;
}, 15*1000);
confCb = function (config) {
confCb = null;
clearTimeout(timeoutId);
resolve(config);
};
});
}
};
function updateConf(config) {
if (confCb) {
confCb(config);
}
}
var userTokens = {
_filename: 'user-tokens.json'
, _cache: {}
, _convertToken: function convertToken(id, token) {
// convert the token into something that looks more like what OAuth3 uses internally
// as sessions so we can use it with OAuth3. We don't use OAuth3's internal session
// storage because it effectively only supports storing tokens based on provider URI.
// We also use the token as the `access_token` instead of `refresh_token` because the
// refresh functionality is closely tied to the storage.
var decoded = jwt.decode(token);
if (!decoded) {
return null;
}
return {
id: id
, access_token: token
, token: decoded
, provider_uri: decoded.iss || decoded.issuer || decoded.provider_uri
, client_uri: decoded.azp
, scope: decoded.scp || decoded.scope || decoded.grants
};
}
, all: function allUserTokens() {
var self = this;
if (self._cacheComplete) {
return deps.PromiseA.resolve(Object.values(self._cache));
}
return read(self._filename).then(function (tokens) {
// We will read every single token into our cache, so it will be complete once we finish
// creating the result (it's set out of order so we can directly return the result).
self._cacheComplete = true;
return Object.keys(tokens).map(function (id) {
self._cache[id] = self._convertToken(id, tokens[id]);
return self._cache[id];
});
});
}
, get: function getUserToken(id) {
var self = this;
if (self._cache.hasOwnProperty(id) || self._cacheComplete) {
return deps.PromiseA.resolve(self._cache[id] || null);
}
return read(self._filename).then(function (tokens) {
self._cache[id] = self._convertToken(id, tokens[id]);
return self._cache[id];
});
}
, save: function saveUserToken(newToken) {
var self = this;
return read(self._filename).then(function (tokens) {
var rawToken;
if (typeof newToken === 'string') {
rawToken = newToken;
} else {
rawToken = newToken.refresh_token || newToken.access_token;
}
if (typeof rawToken !== 'string') {
throw new Error('cannot save invalid session: missing refresh_token and access_token');
}
var decoded = jwt.decode(rawToken);
var idHash = crypto.createHash('sha256');
idHash.update(decoded.sub || decoded.ppid || decoded.appScopedId || '');
idHash.update(decoded.iss || decoded.issuer || '');
idHash.update(decoded.aud || decoded.audience || '');
var scope = decoded.scope || decoded.scp || decoded.grants || '';
idHash.update(scope.split(/[,\s]+/mg).sort().join(','));
var id = idHash.digest('hex');
tokens[id] = rawToken;
return write(self._filename, tokens).then(function () {
// Delete the current cache so that if this is an update it will refresh
// the cache once we read the ID.
delete self._cache[id];
return self.get(id);
});
});
}
, remove: function removeUserToken(id) {
var self = this;
return read(self._filename).then(function (tokens) {
var present = delete tokens[id];
if (!present) {
return present;
}
return write(self._filename, tokens).then(function () {
delete self._cache[id];
return true;
});
});
}
};
@ -99,6 +218,8 @@ module.exports.create = function (deps, conf) {
return {
owners: owners
, config: config
, updateConf: updateConf
, tokens: userTokens
, mdnsId: mdnsId
};
};

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;
@ -65,18 +65,21 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
});
}
function hostMatchesDomains(req, domains) {
function hostMatchesDomains(req, domainList) {
var host = separatePort((req.headers || req).host).host.toLowerCase();
return domains.some(function (pattern) {
return domainList.some(function (pattern) {
return domainMatches(pattern, host);
});
}
function determinePrimaryHost() {
var result;
if (Array.isArray(conf.http.domains)) {
conf.http.domains.some(function (dom) {
if (Array.isArray(conf.domains)) {
conf.domains.some(function (dom) {
if (!dom.modules || !dom.modules.http) {
return false;
}
return dom.names.some(function (domain) {
if (domain[0] !== '*') {
result = domain;
@ -159,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();
@ -169,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);
}
@ -178,7 +181,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
if (headers.url.indexOf('/.well-known/cloud-challenge/') !== 0) {
return false;
}
return emitConnection(deps.loopback.server, conn, opts);
return emitConnection(deps.ddns.loopbackServer, conn, opts);
}
var httpsRedirectServer;
@ -202,17 +205,17 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
var host = separatePort(headers.host).host;
if (!adminDomains) {
adminDomains = require('./admin').adminDomains;
adminDomains = require('../admin').adminDomains;
}
if (adminDomains.indexOf(host) !== -1) {
if (!adminServer) {
adminServer = require('./admin').create(deps, conf);
adminServer = require('../admin').create(deps, conf);
}
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();
@ -238,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) {
@ -289,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) {
@ -466,21 +469,24 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
if (checkAdmin(conn, opts, headers)) { return; }
var prom = PromiseA.resolve(false);
(conf.http.domains || []).forEach(function (dom) {
(conf.domains || []).forEach(function (dom) {
prom = prom.then(function (handled) {
if (handled) {
return handled;
}
if (!dom.modules || !dom.modules.http) {
return false;
}
if (!hostMatchesDomains(headers, dom.names)) {
return false;
}
var subProm = PromiseA.resolve(false);
dom.modules.forEach(function (mod) {
if (moduleChecks[mod.name]) {
dom.modules.http.forEach(function (mod) {
if (moduleChecks[mod.type]) {
subProm = subProm.then(function (handled) {
if (handled) { return handled; }
return moduleChecks[mod.name](mod, conn, opts, headers);
return moduleChecks[mod.type](mod, conn, opts, headers);
});
} else {
console.warn('unknown HTTP module under domains', dom.names.join(','), mod);
@ -498,8 +504,8 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
return false;
}
if (moduleChecks[mod.name]) {
return moduleChecks[mod.name](mod, conn, opts, headers);
if (moduleChecks[mod.type]) {
return moduleChecks[mod.type](mod, conn, opts, headers);
}
console.warn('unknown HTTP module found', mod);
});

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');
@ -27,8 +27,8 @@ module.exports.create = function (deps, config, netHandler) {
return value || '';
}
function nameMatchesDomains(name, domains) {
return domains.some(function (pattern) {
function nameMatchesDomains(name, domainList) {
return domainList.some(function (pattern) {
return domainMatches(pattern, name);
});
}
@ -50,10 +50,7 @@ module.exports.create = function (deps, config, netHandler) {
return;
}
process.nextTick(function () {
socket.unshift(opts.firstChunk);
});
writer.write(opts.firstChunk);
socket.pipe(writer);
writer.pipe(socket);
@ -89,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'
@ -135,14 +131,17 @@ module.exports.create = function (deps, config, netHandler) {
}
var handled = false;
if (Array.isArray(config.tls.domains)) {
handled = config.tls.domains.some(function (dom) {
if (Array.isArray(config.domains)) {
handled = config.domains.some(function (dom) {
if (!dom.modules || !dom.modules.tls) {
return false;
}
if (!nameMatchesDomains(opts.domain, dom.names)) {
return false;
}
return dom.modules.some(function (mod) {
if (mod.name !== 'acme') {
return dom.modules.tls.some(function (mod) {
if (mod.type !== 'acme') {
return false;
}
complete(mod, dom.names);
@ -156,7 +155,7 @@ module.exports.create = function (deps, config, netHandler) {
if (Array.isArray(config.tls.modules)) {
handled = config.tls.modules.some(function (mod) {
if (mod.name !== 'acme') {
if (mod.type !== 'acme') {
return false;
}
if (!nameMatchesDomains(opts.domain, mod.domains)) {
@ -171,26 +170,6 @@ module.exports.create = function (deps, config, netHandler) {
return;
}
var defAcmeConf;
if (config.tls.acme) {
defAcmeConf = config.tls.acme;
} else {
defAcmeConf = {
email: config.tls.email
, server: config.tls.acmeDirectoryUrl || le.server
, challengeType: config.tls.challengeType || le.challengeType
, approvedDomains: config.tls.servernames
};
}
// Check config for domain name
// TODO: if `approvedDomains` isn't defined check all other modules to see if they can
// handle this domain (and what other domains it's grouped with).
if (-1 !== (defAcmeConf.approvedDomains || []).indexOf(opts.domain)) {
complete(defAcmeConf, defAcmeConf.approvedDomains);
return;
}
cb(new Error('domain is not allowed'));
}
});
@ -228,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
@ -252,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});
@ -311,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);
@ -322,20 +301,23 @@ module.exports.create = function (deps, config, netHandler) {
}
function checkModule(mod) {
if (mod.name === 'proxy') {
if (mod.type === 'proxy') {
return proxy(socket, opts, mod);
}
if (mod.name !== 'acme') {
if (mod.type !== 'acme') {
console.error('saw unknown TLS module', mod);
}
}
var handled = (config.tls.domains || []).some(function (dom) {
var handled = (config.domains || []).some(function (dom) {
if (!dom.modules || !dom.modules.tls) {
return false;
}
if (!nameMatchesDomains(opts.servername, dom.names)) {
return false;
}
return dom.modules.some(checkModule);
return dom.modules.tls.some(checkModule);
});
if (handled) {
return;

View File

@ -1,237 +0,0 @@
'use strict';
module.exports.create = function (deps, config) {
var PromiseA = require('bluebird');
var fs = PromiseA.promisifyAll(require('fs'));
var stunnel = require('stunnel');
var activeTunnels = {};
var path = require('path');
var tokensPath = path.join(__dirname, '..', 'var', 'tokens.json');
var storage = {
_read: function () {
var tokens;
try {
tokens = require(tokensPath);
} catch (err) {
tokens = {};
}
return tokens;
}
, _write: function (tokens) {
return fs.mkdirAsync(path.dirname(tokensPath)).catch(function (err) {
if (err.code !== 'EEXIST') {
console.error('failed to mkdir', path.dirname(tokensPath), err.toString());
}
}).then(function () {
return fs.writeFileAsync(tokensPath, JSON.stringify(tokens), 'utf8');
});
}
, _makeKey: function (token) {
// We use a stripped down version of the token contents so that if the token is
// re-issued the nonce and the iat and any other less important things are different
// we don't save essentially duplicate tokens multiple times.
var parsed = JSON.parse((new Buffer(token.split('.')[1], 'base64')).toString());
var stripped = {};
['aud', 'iss', 'domains'].forEach(function (key) {
if (parsed[key]) {
stripped[key] = parsed[key];
}
});
stripped.domains.sort();
var hash = require('crypto').createHash('sha256');
return hash.update(JSON.stringify(stripped)).digest('hex');
}
, all: function () {
var tokens = storage._read();
return PromiseA.resolve(Object.keys(tokens).map(function (key) {
return tokens[key];
}));
}
, save: function (token) {
return PromiseA.resolve().then(function () {
var curTokens = storage._read();
curTokens[storage._makeKey(token.jwt)] = token;
return storage._write(curTokens);
});
}
, del: function (token) {
return PromiseA.resolve().then(function () {
var curTokens = storage._read();
delete curTokens[storage._makeKey(token.jwt)];
return storage._write(curTokens);
});
}
};
function acquireToken(session) {
var OAUTH3 = deps.OAUTH3;
// session seems to be changed by the API call for some reason, so save the
// owner before that happens.
var owner = session.id;
// The OAUTH3 library stores some things on the root session object that we usually
// just leave inside the token, but we need to pull those out before we use it here
session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss;
session.client_uri = session.client_uri || session.token.azp;
session.scope = session.scope || session.token.scp;
console.log('asking for tunnel token from', session.token.aud);
return OAUTH3.discover(session.token.aud).then(function (directives) {
var opts = {
api: 'tunnel.token'
, session: session
, data: {
// filter to all domains that are on this device
//domains: Object.keys(domainsMap)
device: {
hostname: config.device.hostname
, id: config.device.uid || config.device.id
}
}
};
return OAUTH3.api(directives.api, opts).then(function (result) {
console.log('got a token from the tunnel server?');
result.owner = owner;
return result;
});
});
}
function addToken(data) {
if (!data.jwt) {
return PromiseA.reject(new Error("missing 'jwt' from tunnel data"));
}
if (!data.tunnelUrl) {
var decoded;
try {
decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii'));
} catch (err) {
console.warn('invalid web token given to tunnel manager', err);
return PromiseA.reject(err);
}
if (!decoded.aud) {
console.warn('tunnel manager given token with no tunnelUrl or audience');
var err = new Error('missing tunnelUrl and audience');
return PromiseA.reject(err);
}
data.tunnelUrl = 'wss://' + decoded.aud + '/';
}
if (!activeTunnels[data.tunnelUrl]) {
console.log('creating new tunnel client for', data.tunnelUrl);
// We create the tunnel without an initial token so we can append the token and
// 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
// 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
// a '*' sub-property.
, services: {
https: { '*': 443 }
, http: { '*': 80 }
, smtp: { '*': 25 }
, smtps: { '*': 587 /*also 465/starttls*/ }
, ssh: { '*': 22 }
}
});
}
console.log('appending token to tunnel at', data.tunnelUrl);
return activeTunnels[data.tunnelUrl].append(data.jwt);
}
function removeToken(data) {
if (!data.tunnelUrl) {
var decoded;
try {
decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii'));
} catch (err) {
console.warn('invalid web token given to tunnel manager', err);
return PromiseA.reject(err);
}
if (!decoded.aud) {
console.warn('tunnel manager given token with no tunnelUrl or audience');
var err = new Error('missing tunnelUrl and audience');
return PromiseA.reject(err);
}
data.tunnelUrl = 'wss://' + decoded.aud + '/';
}
// Not sure if we actually want to return an error that the token didn't even belong to a
// server that existed, but since it never existed we can consider it as "removed".
if (!activeTunnels[data.tunnelUrl]) {
return PromiseA.resolve();
}
console.log('removing token from tunnel at', data.tunnelUrl);
return activeTunnels[data.tunnelUrl].clear(data.jwt);
}
if (config.tunnel) {
var confTokens = config.tunnel;
if (typeof confTokens === 'string') {
confTokens = confTokens.split(',');
}
confTokens.forEach(function (jwt) {
if (typeof jwt === 'object') {
jwt.owner = 'config';
addToken(jwt);
} else {
addToken({ jwt: jwt, owner: 'config' });
}
});
}
storage.all().then(function (stored) {
stored.forEach(function (result) {
addToken(result);
});
});
return {
start: function (session) {
return acquireToken(session).then(function (token) {
return addToken(token).then(function () {
return storage.save(token);
});
});
}
, add: function (data) {
return addToken(data).then(function () {
return storage.save(data);
});
}
, remove: function (data) {
return storage.del(data.jwt).then(function () {
return removeToken(data);
});
}
, get: function (owner) {
return storage.all().then(function (tokens) {
var result = {};
tokens.forEach(function (data) {
if (!result[data.owner]) {
result[data.owner] = {};
}
if (!result[data.owner][data.tunnelUrl]) {
result[data.owner][data.tunnelUrl] = [];
}
data.decoded = JSON.parse(new Buffer(data.jwt.split('.')[0], 'base64'));
result[data.owner][data.tunnelUrl].push(data);
});
if (owner) {
return result[owner] || {};
}
return result;
});
}
};
};

View File

@ -1,26 +1,10 @@
'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; }
};
}
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) {
function httpsTunnel(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
// 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) {
}
function handleHttp(servername, conn) {
console.error('tunnel server received un-encrypted connection to', servername);
conn.end([
'HTTP/1.1 404 Not Found'
@ -31,31 +15,117 @@ module.exports.create = function (deps, config) {
, ''
, 'Not Found'
].join('\r\n'));
};
tunnelOpts.handleInsecureHttp = tunnelOpts.handleHttp;
var tunnelServer = require('stunneld').create(tunnelOpts);
var httpServer = require('http').createServer(function (req, res) {
}
function rejectNonWebsocket(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);
res.send({error: { message: 'Only websockets accepted for tunnel server' }});
}
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.
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 {
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

@ -1,6 +1,7 @@
'use strict';
var config;
var modules;
// Everything that uses the config should be reading it when relevant rather than
// just at the beginning, so we keep the reference for the main object and just
@ -15,26 +16,47 @@ function update(conf) {
config[key] = conf[key];
}
});
console.log('config', JSON.stringify(config));
console.log('config update', JSON.stringify(config));
Object.values(modules).forEach(function (mod) {
if (typeof mod.updateConf === 'function') {
mod.updateConf(config);
}
});
}
function create(conf) {
var PromiseA = require('bluebird');
var OAUTH3 = require('../packages/assets/org.oauth3');
require('../packages/assets/org.oauth3/oauth3.domains.js');
require('../packages/assets/org.oauth3/oauth3.dns.js');
require('../packages/assets/org.oauth3/oauth3.tunnel.js');
OAUTH3._hooks = require('../packages/assets/org.oauth3/oauth3.node.storage.js');
config = conf;
var deps = {
messenger: process
, PromiseA: require('bluebird')
, PromiseA: PromiseA
, OAUTH3: OAUTH3
, request: PromiseA.promisify(require('request'))
, recase: require('recase').create({})
// Note that if a custom createConnections is used it will be called with different
// sets of custom options based on what is actually being proxied. Most notably the
// HTTP proxying connection creation is not something we currently control.
, net: require('net')
};
deps.storage = require('./storage').create(deps, conf);
deps.proxy = require('./proxy-conn').create(deps, conf);
deps.socks5 = require('./socks5-server').create(deps, conf);
deps.loopback = require('./loopback').create(deps, conf);
deps.ddns = require('./ddns').create(deps, conf);
require('./goldilocks.js').create(deps, conf);
modules = {
storage: require('./storage').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);
process.removeListener('message', create);
process.on('message', update);
}

2263
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,14 @@
{
"name": "goldilocks",
"version": "1.0.0-placeholder",
"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.",
"main": "bin/goldilocks.js",
"repository": {
"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/)",
"license": "SEE LICENSE IN LICENSE.txt",
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)",
"scripts": {
"test": "node bin/goldilocks.js -p 8443 -d /tmp/"
},
@ -34,39 +34,41 @@
"server"
],
"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#readme",
"homepage": "https://git.coolaj86.com/coolaj86/goldilocks.js",
"dependencies": {
"bluebird": "^3.4.6",
"body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1",
"body-parser": "1",
"commander": "^2.9.0",
"dns-suite": "git+https://git@git.daplie.com/Daplie/dns-suite#v1",
"express": "git+https://github.com/expressjs/express.git#4.x",
"deep-equal": "^1.0.1",
"dns-suite": "1",
"express": "4",
"finalhandler": "^0.4.0",
"greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master",
"greenlock": "2.1",
"http-proxy": "^1.16.2",
"human-readable-ids": "git+https://git.daplie.com/Daplie/human-readable-ids-js#master",
"ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0",
"human-readable-ids": "1",
"ipaddr.js": "v1.3",
"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-fs": "2",
"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",
"recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4",
"network": "^0.4.0",
"recase": "v1.0.4",
"redirect-https": "^1.1.0",
"request": "^2.81.0",
"scmp": "git+https://github.com/freewil/scmp.git#1.x",
"scmp": "1",
"serve-index": "^1.7.0",
"serve-static": "^1.10.0",
"server-destroy": "^1.0.1",
"sni": "^1.0.0",
"socket-pair": "^1.0.1",
"socket-pair": "^1.0.3",
"socksv5": "0.0.6",
"stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1",
"stunneld": "git+https://git.daplie.com/Daplie/node-tunnel-server.git#v1",
"stunnel": "1.0",
"stunneld": "0.9",
"tunnel-packer": "^1.3.0",
"ws": "^2.3.1"
}

View File

@ -1,23 +0,0 @@
'use strict';
var api = require('./index.js').api;
var OAUTH3 = require('../../assets/org.oauth3/');
// these all auto-register
require('../../assets/org.oauth3/oauth3.domains.js');
require('../../assets/org.oauth3/oauth3.dns.js');
require('../../assets/org.oauth3/oauth3.tunnel.js');
OAUTH3._hooks = require('../../assets/org.oauth3/oauth3.node.storage.js');
api.tunnel(
{
OAUTH3: OAUTH3
, options: {
device: {
hostname: 'test.local'
, id: ''
}
}
}
// OAUTH3.hooks.session.get('oauth3.org').then(function (result) { console.log(result) });
, require('./test.session.json')
);

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