Merge branch 'ddns'

# Conflicts:
#	README.md
#	etc/goldilocks/goldilocks.example.yml
This commit is contained in:
tigerbot 2017-10-25 18:35:07 -06:00
commit e504c4b717
23 changed files with 2220 additions and 1322 deletions

View File

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

118
API.md
View File

@ -2,30 +2,112 @@
The API system is intended for use with Desktop and Mobile clients. 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: It must be accessed using one of the following domains as the Host header:
``` * localhost.alpha.daplie.me
admin.invalid * localhost.admin.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. All requests require an OAuth3 token in the request headers.
## Tunnel ## Config
### Check Status ### Get All Settings
* **URL** `/api/goldilocks@daplie.com/tunnel` * **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` * **Method** `POST`
* **Reponse**: An object whose keys are the URLs for the tunnels, and whose * **Body**: The changes to be applied on top of the current config. See the
properties are arrays of the tunnel tokens. [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.
This route with return only the sessions started by the same user who is ### Add Module
checking the status. * **URL** `/api/goldilocks@daplie.com/config/:group/modules`
* **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group`
### Start Tunnel
* **URL** `/api/goldilocks@daplie.com/tunnel`
* **Method** `POST` * **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 ## Socks5 Proxy
@ -39,9 +121,9 @@ All requests require an OAuth3 token in the request headers.
### Start Proxy ### Start Proxy
* **URL** `/api/goldilocks@daplie.com/socks5` * **URL** `/api/goldilocks@daplie.com/socks5`
* **Method** `POST` * **Method** `POST`
* **Response**: Same response as for the `GET` resquest * **Response**: Same response as for the `GET` request
### Stop Proxy ### Stop Proxy
* **URL** `/api/goldilocks@daplie.com/socks5` * **URL** `/api/goldilocks@daplie.com/socks5`
* **Method** `DELETE` * **Method** `DELETE`
* **Response**: Same response as for the `GET` resquest * **Response**: Same response as for the `GET` request

418
README.md
View File

@ -53,48 +53,89 @@ curl https://git.daplie.com/Daplie/goldilocks.js/raw/master/install.sh | bash
Modules & Configuration 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](#http)
* http - [proxy (reverse proxy)](#httpproxy-how-to-reverse-proxy-ruby-python-etc)
- static - [static](#httpstatic-how-to-serve-a-web-page)
- redirect - [redirect](#httpredirect-how-to-redirect-urls)
- proxy (reverse proxy) * [tls](#tls)
* tls - [proxy (reverse proxy)](#tlsproxy)
- acme - [acme](#tlsacme)
- proxy (reverse proxy) * [tcp](#tcp)
* tcp - [forward](#tcpforward)
- forward * [udp](#udp)
* tunnel_server - [forward](#udpforward)
* tunnel_client * [domains](#domains)
* mdns * [tunnel_server](#tunnel_server)
* [tunnel_client](#tunnel)
* [mdns](#mdns)
* [socks5](#socks5)
* api * 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 ### http
The HTTP system handles plain http (TLS / SSL is handled by the tls system) The HTTP system handles plain http (TLS / SSL is handled by the tls system)
Example config:
```yml ```yml
http: http:
trust_proxy: true # allow localhost, 192.x, 10.x, 172.x, etc to set headers 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 allow_insecure: false # allow non-https even without proxy https headers
primary_domain: example.com # attempts to access via IP address will redirect here primary_domain: example.com # attempts to access via IP address will redirect here
# modules can be nested in domains # An array of modules that define how to handle incoming HTTP requests
domains:
- names:
- example.com
modules: modules:
- name: static - type: static
domains:
- example.com
root: /srv/www/:hostname 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: modules:
- name: static - type: proxy
domains: domains:
- example.com - api.example.com
root: /srv/www/:hostname 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 ### http.static - how to serve a web page
@ -121,50 +162,20 @@ indexes An array of directories which should be have indexes served rather t
``` ```
Example config: Example config:
```yml ```yml
http: http:
modules: modules:
- name: static - type: static
domains: domains:
- example.com - example.com
root: /srv/www/:hostname 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 ### http.redirect - how to redirect URLs
The redirect module is for, you guessed it, redirecting URLs. The redirect module is for, you guessed it, redirecting URLs.
It has the following options: It has the following options:
``` ```
status The HTTP status code to issue (301 is usual permanent redirect, 302 is temporary) status The HTTP status code to issue (301 is usual permanent redirect, 302 is temporary)
ex: 301 ex: 301
@ -182,11 +193,10 @@ to The new URL path which should be used.
``` ```
Example config: Example config:
```yml ```yml
http: http:
modules: modules:
- name: proxy - type: proxy
domains: domains:
- example.com - example.com
status: 301 status: 301
@ -197,41 +207,14 @@ http:
### tls ### tls
The tls system handles encrypted connections, including fetching certificates, The tls system handles encrypted connections, including fetching certificates,
and uses ServerName Indication (SNI) to determine if the connection should be handled and uses ServerName Indication (SNI) to determine if the connection should be
by the http system, a tls system module, or rejected. 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
```
Example config: Example config:
```yml ```yml
tls: 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: modules:
- name: proxy - type: proxy
domains: domains:
- example.com - example.com
- example.net - example.net
@ -240,17 +223,44 @@ tls:
Certificates are saved to `~/acme`, which may be `/var/www/acme` if Goldilocks is run as the www-data user. 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` The proxy module routes the traffic based on the ServerName Indication (SNI) **without decrypting** it.
(in favor of the domains in the scope of the module).
It has the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc).
Example config: Example config:
```yml ```yml
tls: tls:
modules: 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: domains:
- example.com - example.com
- example.net - example.net
@ -259,41 +269,18 @@ tls:
challenge_type: 'http-01' 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'
```
### tcp ### tcp
The tcp system handles all tcp network traffic **before decryption** and may use port numbers The tcp system handles all tcp network traffic **before decryption** and may use port numbers
or traffic sniffing to determine how the connection should be handled. or traffic sniffing to determine how the connection should be handled.
It has the following options: It has the following options:
``` ```
bind An array of numeric ports on which to bind bind An array of numeric ports on which to bind
ex: 80 ex: 80
``` ```
Example Config Example Config:
```yml ```yml
tcp: tcp:
bind: bind:
@ -301,7 +288,7 @@ tcp:
- 80 - 80
- 443 - 443
modules: modules:
- name: forward - type: forward
ports: ports:
- 22 - 22
address: '127.0.0.1:2222' address: '127.0.0.1:2222'
@ -311,18 +298,15 @@ tcp:
The forward module routes traffic based on port number **without decrypting** it. 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 ports A numeric array of source ports
ex: 22 ex: 22
address The destination hostname and port
ex: 127.0.0.1:2222
``` ```
Example Config Example Config:
```yml ```yml
tcp: tcp:
bind: bind:
@ -330,10 +314,79 @@ tcp:
- 80 - 80
- 443 - 443
modules: modules:
- name: forward - type: forward
ports: ports:
- 22 - 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.
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
``` ```
@ -363,66 +416,50 @@ tunnel_server:
- 'api.tunnel.example.com' - 'api.tunnel.example.com'
``` ```
### tunnel ### DDNS
The tunnel client is meant to be run from behind a firewalls, carrier-grade NAT, The DDNS module watches the network environment of the unit and makes sure the
or otherwise inaccessible devices to allow them to be accessed publicly on the device is always accessible on the internet using the domains listed in the
internet. 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 The `loopback` setting specifies how the unit will check its public IP address
connect to tunnel servers. If the token does not have an `aud` field it must be and whether connections can reach it. Currently only `tunnel@oauth3.org` is
provided in an object with the token provided in the `jwt` field and the tunnel supported. If the loopback setting is not defined it will default to using
server url provided in the `tunnelUrl` field. `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
tunnel: up port forwarding (and connect to a tunnel if that doesn't work), but the DNS
- 'some.jwt_encoded.token' records will not be updated to point to the device. This is to allow a setup to
- jwt: 'other.jwt_encoded.token' be tested before transitioning services between devices.
tunnelUrl: 'wss://api.tunnel.example.com/'
```yaml
ddns:
disabled: false
loopback:
type: 'tunnel@oauth3.org'
domain: oauth3.org
tunnel:
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 ### mDNS
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
enabled by default enabled by default
@ -465,15 +502,14 @@ See [API.md](/API.md)
TODO TODO
---- ----
* http - nowww module * [ ] http - nowww module
* http - Allow match styles of `www.*`, `*`, and `*.example.com` equally * [ ] http - Allow match styles of `www.*`, `*`, and `*.example.com` equally
* http - redirect based on domain name (not just path) * [ ] http - redirect based on domain name (not just path)
* tcp - bind should be able to specify localhost, uniquelocal, private, or ip * [ ] tcp - bind should be able to specify localhost, uniquelocal, private, or ip
* tcp - if destination host is omitted default to localhost, if dst port is missing, default to src * [ ] tcp - if destination host is omitted default to localhost, if dst port is missing, default to src
* sys - handle SIGHUP * [ ] sys - `curl https://daplie.me/goldilocks | bash -s example.com`
* sys - `curl https://daplie.me/goldilocks | bash -s example.com` * [ ] oauth3 - `example.com/.well-known/domains@oauth3.org/directives.json`
* oauth3 - `example.com/.well-known/domains@oauth3.org/directives.json` * [ ] oauth3 - commandline questionnaire
* oauth3 - commandline questionnaire * [x] modules - use consistent conventions (i.e. address vs host + port)
* modules - use consistent conventions (i.e. address vs host + port) * [x] tls - tls.acme vs tls.modules.acme
* tls - tls.acme vs tls.modules.acme * [ ] tls - forward should be able to match on source port to reach different destination ports
* 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; return;
} }
var crypto = require('crypto');
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var fs = PromiseA.promisifyAll(require('fs')); var fs = PromiseA.promisifyAll(require('fs'));
var configStorage; 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 recase = require('recase').create({});
var snakeCopy = recase.snakeCopy.bind(recase); var snakeCopy = recase.snakeCopy.bind(recase);
var camelCopy = recase.camelCopy.bind(recase); var camelCopy = recase.camelCopy.bind(recase);
@ -40,13 +192,25 @@ function createStorage(filename, filetype) {
dump = yaml.safeDump; dump = yaml.safeDump;
} }
function read() { async function read() {
return fs.readFileAsync(filename).then(parse).catch(function (err) { var text;
try {
text = await fs.readFileAsync(filename);
} catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
return {}; 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 = { var result = {
@ -76,51 +240,53 @@ function createStorage(filename, filetype) {
}; };
return result; return result;
} }
function checkConfigLocation(cwd, configFile) { async function checkConfigLocation(cwd, configFile) {
cwd = cwd || process.cwd(); cwd = cwd || process.cwd();
var path = require('path'); var path = require('path');
var filename; var filename, text;
var prom;
if (configFile) { if (configFile) {
filename = path.resolve(cwd, configFile); filename = path.resolve(cwd, configFile);
prom = fs.readFileAsync(filename) try {
.catch(function (err) { text = await fs.readFileAsync(filename);
} catch (err) {
if (err.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
return PromiseA.reject(err); throw err;
} }
if (path.extname(filename) === '.json') { if (path.extname(filename) === '.json') {
return '{}'; return { name: filename, type: 'json' };
}
return '';
})
;
} else { } else {
prom = PromiseA.reject('blah') return { name: filename, type: 'yaml' };
.catch(function () { }
filename = path.resolve(cwd, 'goldilocks.yml'); }
return fs.readFileAsync(filename); } else {
}) // Note that `path.resolve` can handle both relative and absolute paths.
.catch(function () { var defLocations = [
filename = path.resolve(cwd, 'goldilocks.json'); path.resolve(cwd, 'goldilocks.yml')
return fs.readFileAsync(filename); , path.resolve(cwd, 'goldilocks.json')
}) , path.resolve(cwd, 'etc/goldilocks/goldilocks.yml')
.catch(function () { , '/etc/goldilocks/goldilocks.yml'
filename = path.resolve(cwd, 'etc/goldilocks/goldilocks.yml'); ];
return fs.readFileAsync(filename);
}) var ind;
.catch(function () { for (ind = 0; ind < defLocations.length; ind += 1) {
filename = '/etc/goldilocks/goldilocks.yml'; try {
return fs.readFileAsync(filename); text = await fs.readFileAsync(defLocations[ind]);
}) filename = defLocations[ind];
.catch(function () { break;
filename = path.resolve(cwd, 'goldilocks.yml'); } catch (err) {
return ''; if (err.code !== 'ENOENT') {
}) throw err;
; }
}
}
if (!filename) {
filename = defLocations[0];
text = '';
}
} }
return prom.then(function (text) {
try { try {
JSON.parse(text); JSON.parse(text);
return { name: filename, type: 'json' }; return { name: filename, type: 'json' };
@ -132,89 +298,50 @@ function checkConfigLocation(cwd, configFile) {
} catch (err) {} } catch (err) {}
throw new Error('Could not load "' + filename + '" as JSON nor YAML'); throw new Error('Could not load "' + filename + '" as JSON nor YAML');
});
} }
function createConfigStorage(args) { async function createConfigStorage(args) {
return checkConfigLocation(args.cwd, args.config) var result = await checkConfigLocation(args.cwd, args.config);
.then(function (result) {
console.log('config file', result.name, 'is of type', result.type); 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(); return configStorage.read();
})
;
} }
var tcpProm; var tcpProm;
function fillConfig(config, args) { function fillConfig(config, args) {
config.debug = config.debug || args.debug; config.debug = config.debug || args.debug;
if (!config.dns) { config.socks5 = config.socks5 || { enabled: false };
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);
if (!config.tcp) { // Use Object.assign to copy any real config values over the default values so we can
config.tcp = {}; // easily make sure all the fields we need exist .
} var mdnsDefaults = { disabled: false, port: 5353, broadcast: '224.0.0.251', ttl: 300 };
if (!config.http) { config.mdns = Object.assign(mdnsDefaults, config.mdns);
config.http = { modules: [{ name: 'proxy', domains: ['*'], port: 3000 }] };
} if (!Array.isArray(config.domains)) {
if (!config.tls) { config.domains = [];
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;
} }
// maybe this should not go in config... but be ephemeral in some way? function fillComponent(name, fillBind) {
config.cwd = args.cwd || config.cwd || process.cwd(); if (!config[name]) {
config[name] = {};
var ipaddr = require('ipaddr.js'); }
var addresses = []; if (!Array.isArray(config[name].modules)) {
var ifaces = require('../lib/local-ip.js').find(); config[name].modules = [];
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;
} }
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.device = { hostname: require('os').hostname() };
config.tunnel = args.tunnel || config.tunnel; if (Array.isArray(config.tcp.bind) && config.tcp.bind.length) {
if (Array.isArray(config.tcp.bind)) {
return PromiseA.resolve(config); return PromiseA.resolve(config);
} }
@ -297,6 +424,9 @@ function run(args) {
// TODO spin up multiple workers // TODO spin up multiple workers
// TODO use greenlock-cluster // TODO use greenlock-cluster
cluster.fork(); cluster.fork();
}).catch(function (err) {
console.error(err);
process.exit(1);
}) })
; ;
} }
@ -310,9 +440,7 @@ function readEnv(args) {
} catch (err) {} } catch (err) {}
var env = { var env = {
tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true cwd: process.env.GOLDILOCKS_HOME || process.cwd()
, email: process.env.GOLDILOCKS_EMAIL
, cwd: process.env.GOLDILOCKS_HOME || process.cwd()
, debug: process.env.GOLDILOCKS_DEBUG && true , debug: process.env.GOLDILOCKS_DEBUG && true
}; };
@ -323,10 +451,7 @@ var program = require('commander');
program program
.version(require('../package.json').version) .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('-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") .option('--debug', "Enable debug output")
.parse(process.argv); .parse(process.argv);

View File

@ -4,90 +4,103 @@ tcp:
- 80 - 80
- 443 - 443
modules: modules:
- name: forward - type: forward
ports: ports:
- 22 - 22
address: '127.0.0.1:8022' address: '127.0.0.1:8022'
# tunnel: jwt udp:
# tunnel: bind:
# - jwt1 - 53
# - jwt2 modules:
- type: forward
ports:
- 53
port: 5353
# default host is localhost
tunnel_server:
secret: abc123
servernames:
- 'tunnel.localhost.com'
tls: 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: modules:
- name: proxy - type: 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
domains: domains:
- localhost.bar.daplie.me - localhost.bar.daplie.me
- localhost.foo.daplie.me - localhost.foo.daplie.me
address: '127.0.0.1:5443' address: '127.0.0.1:5443'
- name: acme - type: acme
domains:
- '*.localhost.daplie.me'
email: 'guest@example.com' email: 'guest@example.com'
challenge_type: 'http-01' challenge_type: 'http-01'
domains:
- foo.localhost.daplie.me
- gamma.localhost.daplie.me
http: http:
trust_proxy: true trust_proxy: true
allow_insecure: false allow_insecure: false
primary_domain: localhost.foo.daplie.me primary_domain: localhost.daplie.me
domains:
- names:
- localhost.baz.daplie.me
modules:
- name: redirect
from: /nowhere/in/particular
to: /just/an/example
- name: proxy
port: 3001
modules: modules:
- name: redirect - type: redirect
domains: domains:
- localhost.beta.daplie.me - localhost.beta.daplie.me
status: 301 status: 301
from: /old/path/*/other/* from: /old/path/*/other/*
to: https://example.com/path/new/:2/something/:1 to: https://example.com/path/new/:2/something/:1
- name: proxy - type: proxy
domains: domains:
- localhost.daplie.me - localhost.daplie.me
host: localhost host: localhost
port: 4000 port: 4000
- name: static - type: static
domains: domains:
- '*.localhost.daplie.me' - '*.localhost.daplie.me'
root: '/srv/www/:hostname' 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: mdns:
disabled: false disabled: false
port: 5353 port: 5353
broadcast: '224.0.0.251' broadcast: '224.0.0.251'
ttl: 300 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

@ -21,6 +21,7 @@ module.exports.create = function (deps, conf) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*'); res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', methods.join(', ')); res.setHeader('Access-Control-Allow-Methods', methods.join(', '));
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method.toUpperCase() === 'OPTIONS') { if (req.method.toUpperCase() === 'OPTIONS') {
res.setHeader('Allow', methods.join(', ')); res.setHeader('Allow', methods.join(', '));
@ -38,6 +39,26 @@ module.exports.create = function (deps, conf) {
return true; 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) { function isAuthorized(req, res, fn) {
var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, '')); 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) { init: function (req, res) {
if (handleCors(req, res, ['GET', 'POST'])) { if (handleCors(req, res, ['GET', 'POST'])) {
return; 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) { , request: function (req, res) {
if (handleCors(req, res, '*')) { if (handleCors(req, res, '*')) {
return; 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) { , paywall_check: function (req, res) {
if (handleCors(req, res, 'GET')) { if (handleCors(req, res, 'GET')) {
return; 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;
}; };

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

@ -0,0 +1,379 @@
'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 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)
};
function addDomainRequirement(itemSchema) {
itemSchema.required = (itemSchema.required || []).concat('domains');
itemSchema.properties = itemSchema.properties || {};
itemSchema.properties.domains = { type: 'array', items: { type: 'string' }, minLength: 1};
return itemSchema;
}
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 }}
}
, 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 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: { 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
}
, 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)
};
});
}
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()
};
// 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
};
};

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

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

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

@ -0,0 +1,329 @@
'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 loopback = require('./loopback').create(deps, conf);
var dnsCtrl = require('./dns-ctrl').create(deps, conf);
var equal = require('deep-equal');
var loopbackDomain;
function iterateAllModules(action, curConf) {
curConf = curConf || conf;
var promises = curConf.ddns.modules.map(function (mod) {
return action(mod, mod.domains);
});
curConf.domains.forEach(function (dom) {
if (!dom.modules || !Array.isArray(dom.modules.ddns) || !dom.modules.ddns.length) {
return null;
}
// For the time being all of our things should only be tried once (regardless if it succeeded)
// TODO: revisit this behavior when we support multiple ways of setting records, and/or
// if we want to allow later modules to run if early modules fail.
promises.push(dom.modules.ddns.reduce(function (prom, mod) {
if (prom) { return prom; }
return action(mod, dom.names);
}, null));
});
return deps.PromiseA.all(promises.filter(Boolean));
}
async function getSession(id) {
var session = await deps.storage.tokens.get(id);
if (!session) {
throw new Error('no user token with ID "'+id+'"');
}
return session;
}
var tunnelActive = false;
async function startTunnel(tunnelSession, mod, domainList) {
try {
var dnsSession = await getSession(mod.tokenId);
var tunnelDomain = await deps.tunnelClients.start(tunnelSession || dnsSession, domainList);
var 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 + '"');
}
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 iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return null; }
return startTunnel(tunnelSession, mod, domainList);
});
tunnelActive = true;
}
async function disconnectTunnels() {
deps.tunnelClients.disconnect();
tunnelActive = false;
await Promise.resolve();
}
async function checkTunnelTokens() {
var oldTokens = deps.tunnelClients.current();
var newTokens = await 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(deps.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 gw = await network.getGatewayIpAsync();
var addr = await network.getPrivateIpAsync();
if (localAddr === addr && gateway === gw) {
return;
}
localAddr = addr;
gateway = gw;
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.
if (!notLooped.length) {
if (tunnelActive) {
await disconnectTunnels();
}
} else {
if (!tunnelActive) {
await connectAllTunnels();
}
}
}
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 iterateAllModules(function setModuleDNS(mod, domainList) {
if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; }
return 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.
iterateAllModules(function (mod, domainList) {
if (mod.type !== 'dns@oauth3.org') { return; }
prevMods[mod.id] = { mod, domainList };
return true;
}, prevConf);
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}) {
var oldDomains;
if (!curMods[mod.id] || 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 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}) {
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 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
};
};

View File

@ -1,14 +1,12 @@
'use strict'; 'use strict';
module.exports.create = function (deps, conf) { module.exports.create = function (deps, conf) {
var PromiseA = require('bluebird');
var request = PromiseA.promisify(require('request'));
var pending = {}; var pending = {};
async function checkPublicAddr(host) { async function _checkPublicAddr(host) {
var result = await request({ var result = await deps.request({
method: 'GET' method: 'GET'
, url: host+'/api/org.oauth3.tunnel/checkip' , url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/checkip'
, json: true , json: true
}); });
@ -21,6 +19,10 @@ module.exports.create = function (deps, conf) {
} }
return result.body.address; 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) { async function checkSinglePort(host, address, port) {
var crypto = require('crypto'); var crypto = require('crypto');
@ -30,7 +32,7 @@ module.exports.create = function (deps, conf) {
var reqObj = { var reqObj = {
method: 'POST' method: 'POST'
, url: host+'/api/org.oauth3.tunnel/loopback' , url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/loopback'
, json: { , json: {
address: address address: address
, port: port , port: port
@ -42,7 +44,7 @@ module.exports.create = function (deps, conf) {
var result; var result;
try { try {
result = await request(reqObj); result = await deps.request(reqObj);
} catch (err) { } catch (err) {
delete pending[token]; delete pending[token];
throw err; throw err;
@ -63,11 +65,13 @@ module.exports.create = function (deps, conf) {
async function loopback(provider) { async function loopback(provider) {
var directives = await deps.OAUTH3.discover(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'); console.log('checking to see if', address, 'gets back to us');
}
var ports = require('./servers').listeners.tcp.list(); var ports = require('../servers').listeners.tcp.list();
var values = await PromiseA.all(ports.map(function (port) { var values = await deps.PromiseA.all(ports.map(function (port) {
return checkSinglePort(directives.api, address, port); return checkSinglePort(directives.api, address, port);
})); }));
@ -75,11 +79,13 @@ module.exports.create = function (deps, conf) {
console.log('remaining loopback tokens', pending); console.log('remaining loopback tokens', pending);
} }
var result = {error: null, address: address}; return {
ports.forEach(function (port, ind) { address: address
result[port] = values[ind]; , ports: ports.reduce(function (obj, port, ind) {
}); obj[port] = values[ind];
return result; return obj;
}, {})
};
} }
loopback.checkPublicAddr = checkPublicAddr; loopback.checkPublicAddr = checkPublicAddr;

View File

@ -95,16 +95,20 @@ module.exports.create = function (deps, config) {
}); });
} }
function dnsListener(msg) { function dnsListener(port, msg) {
if (!Array.isArray(config.dns.modules)) { if (!Array.isArray(config.udp.modules)) {
return; return;
} }
var socket = require('dgram').createSocket('udp4'); var socket = require('dgram').createSocket('udp4');
config.dns.modules.forEach(function (mod) { config.udp.modules.forEach(function (mod) {
if (mod.name !== 'proxy') { if (mod.type !== 'forward') {
console.warn('found bad DNS module', mod); console.warn('found bad DNS module', mod);
return; return;
} }
if (mod.ports.indexOf(port) < 0) {
return;
}
var dest = require('./domain-utils').separatePort(mod.address || ''); var dest = require('./domain-utils').separatePort(mod.address || '');
dest.port = dest.port || mod.port; dest.port = dest.port || mod.port;
dest.host = dest.host || mod.host || 'localhost'; dest.host = dest.host || mod.host || 'localhost';
@ -197,23 +201,12 @@ module.exports.create = function (deps, config) {
var listenPromises = []; var listenPromises = [];
var tcpPortMap = {}; var tcpPortMap = {};
function addPorts(bindList) { config.tcp.bind.filter(Number).forEach(function (port) {
if (!bindList) {
return;
}
if (Array.isArray(bindList)) {
bindList.filter(Number).forEach(function (port) {
tcpPortMap[port] = true; tcpPortMap[port] = true;
}); });
}
else if (Number(bindList)) {
tcpPortMap[bindList] = true;
}
}
addPorts(config.tcp.bind);
(config.tcp.modules || []).forEach(function (mod) { (config.tcp.modules || []).forEach(function (mod) {
if (mod.name === 'forward') { if (mod.type === 'forward') {
var forwarder = createTcpForwarder(mod); var forwarder = createTcpForwarder(mod);
mod.ports.forEach(function (port) { mod.ports.forEach(function (port) {
if (!tcpPortMap[port]) { if (!tcpPortMap[port]) {
@ -229,25 +222,15 @@ module.exports.create = function (deps, config) {
} }
}); });
// 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(); var portList = Object.keys(tcpPortMap).map(Number).sort();
portList.forEach(function (port) { portList.forEach(function (port) {
listenPromises.push(listeners.tcp.add(port, netHandler)); listenPromises.push(listeners.tcp.add(port, netHandler));
}); });
if (config.dns.bind) { if (config.udp.bind) {
if (Array.isArray(config.dns.bind)) { config.udp.bind.forEach(function (port) {
config.dns.bind.map(function (port) { listenPromises.push(listeners.udp.add(port, dnsListener.bind(port)));
listenPromises.push(listeners.udp.add(port, dnsListener));
}); });
} else {
listenPromises.push(listeners.udp.add(config.dns.bind, dnsListener));
}
} }
if (!config.mdns.disabled) { if (!config.mdns.disabled) {

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

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

View File

@ -27,8 +27,8 @@ module.exports.create = function (deps, config, netHandler) {
return value || ''; return value || '';
} }
function nameMatchesDomains(name, domains) { function nameMatchesDomains(name, domainList) {
return domains.some(function (pattern) { return domainList.some(function (pattern) {
return domainMatches(pattern, name); return domainMatches(pattern, name);
}); });
} }
@ -50,10 +50,7 @@ module.exports.create = function (deps, config, netHandler) {
return; return;
} }
process.nextTick(function () { writer.write(opts.firstChunk);
socket.unshift(opts.firstChunk);
});
socket.pipe(writer); socket.pipe(writer);
writer.pipe(socket); writer.pipe(socket);
@ -135,14 +132,17 @@ module.exports.create = function (deps, config, netHandler) {
} }
var handled = false; var handled = false;
if (Array.isArray(config.tls.domains)) { if (Array.isArray(config.domains)) {
handled = config.tls.domains.some(function (dom) { handled = config.domains.some(function (dom) {
if (!dom.modules || !dom.modules.tls) {
return false;
}
if (!nameMatchesDomains(opts.domain, dom.names)) { if (!nameMatchesDomains(opts.domain, dom.names)) {
return false; return false;
} }
return dom.modules.some(function (mod) { return dom.modules.tls.some(function (mod) {
if (mod.name !== 'acme') { if (mod.type !== 'acme') {
return false; return false;
} }
complete(mod, dom.names); complete(mod, dom.names);
@ -156,7 +156,7 @@ module.exports.create = function (deps, config, netHandler) {
if (Array.isArray(config.tls.modules)) { if (Array.isArray(config.tls.modules)) {
handled = config.tls.modules.some(function (mod) { handled = config.tls.modules.some(function (mod) {
if (mod.name !== 'acme') { if (mod.type !== 'acme') {
return false; return false;
} }
if (!nameMatchesDomains(opts.domain, mod.domains)) { if (!nameMatchesDomains(opts.domain, mod.domains)) {
@ -171,26 +171,6 @@ module.exports.create = function (deps, config, netHandler) {
return; 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')); cb(new Error('domain is not allowed'));
} }
}); });
@ -322,20 +302,23 @@ module.exports.create = function (deps, config, netHandler) {
} }
function checkModule(mod) { function checkModule(mod) {
if (mod.name === 'proxy') { if (mod.type === 'proxy') {
return proxy(socket, opts, mod); return proxy(socket, opts, mod);
} }
if (mod.name !== 'acme') { if (mod.type !== 'acme') {
console.error('saw unknown TLS module', mod); 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)) { if (!nameMatchesDomains(opts.servername, dom.names)) {
return false; return false;
} }
return dom.modules.some(checkModule); return dom.modules.tls.some(checkModule);
}); });
if (handled) { if (handled) {
return; return;

View File

@ -3,6 +3,8 @@
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var path = require('path'); var path = require('path');
var fs = PromiseA.promisifyAll(require('fs')); var fs = PromiseA.promisifyAll(require('fs'));
var jwt = require('jsonwebtoken');
var crypto = require('crypto');
module.exports.create = function (deps, conf) { module.exports.create = function (deps, conf) {
var hrIds = require('human-readable-ids').humanReadableIds; var hrIds = require('human-readable-ids').humanReadableIds;
@ -65,12 +67,129 @@ module.exports.create = function (deps, conf) {
} }
}; };
var confCb;
var config = { var config = {
save: function (changes) { save: function (changes) {
deps.messenger.send({ deps.messenger.send({
type: 'com.daplie.goldilocks/config' type: 'com.daplie.goldilocks/config'
, changes: changes , 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 { return {
owners: owners owners: owners
, config: config , config: config
, updateConf: updateConf
, tokens: userTokens
, mdnsId: mdnsId , mdnsId: mdnsId
}; };
}; };

View File

@ -1,126 +1,69 @@
'use strict'; 'use strict';
module.exports.create = function (deps, config) { module.exports.create = function (deps, config) {
var PromiseA = require('bluebird');
var fs = PromiseA.promisifyAll(require('fs'));
var stunnel = require('stunnel'); var stunnel = require('stunnel');
var jwt = require('jsonwebtoken');
var activeTunnels = {}; var activeTunnels = {};
var activeDomains = {};
var path = require('path'); function fillData(data) {
var tokensPath = path.join(__dirname, '..', 'var', 'tokens.json'); if (typeof data === 'string') {
var storage = { data = { jwt: data };
_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) { if (!data.jwt) {
return PromiseA.reject(new Error("missing 'jwt' from tunnel data")); 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 (!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) { if (!decoded.aud) {
console.warn('tunnel manager given token with no tunnelUrl or audience'); throw new Error('missing tunnelUrl and audience');
var err = new Error('missing tunnelUrl and audience');
return PromiseA.reject(err);
} }
data.tunnelUrl = 'wss://' + decoded.aud + '/'; 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]) { if (!activeTunnels[data.tunnelUrl]) {
console.log('creating new tunnel client for', 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 // We create the tunnel without an initial token so we can append the token and
@ -142,96 +85,61 @@ module.exports.create = function (deps, config) {
}); });
} }
console.log('appending token to tunnel at', data.tunnelUrl); console.log('appending token to tunnel at', data.tunnelUrl, 'for domains', data.domains);
return activeTunnels[data.tunnelUrl].append(data.jwt); 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(/\/.*/, '');
} }
function removeToken(data) { async function acquireToken(session, domains) {
if (!data.tunnelUrl) { var OAUTH3 = deps.OAUTH3;
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 // The OAUTH3 library stores some things on the root session object that we usually
// server that existed, but since it never existed we can consider it as "removed". // just leave inside the token, but we need to pull those out before we use it here
if (!activeTunnels[data.tunnelUrl]) { session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss;
return PromiseA.resolve(); session.client_uri = session.client_uri || session.token.azp;
} session.scope = session.scope || session.token.scp;
console.log('removing token from tunnel at', data.tunnelUrl); console.log('asking for tunnel token from', session.token.aud);
return activeTunnels[data.tunnelUrl].clear(data.jwt); var opts = {
api: 'tunnel.token'
, session: session
, data: {
domains: domains
, device: {
hostname: config.device.hostname
, id: config.device.uid || config.device.id
} }
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;
});
} }
}; };
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
};
}; };

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
var config; var config;
var modules;
// Everything that uses the config should be reading it when relevant rather than // 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 // just at the beginning, so we keep the reference for the main object and just
@ -15,24 +16,43 @@ function update(conf) {
config[key] = conf[key]; 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) { 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; config = conf;
var deps = { var deps = {
messenger: process 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 // 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 // sets of custom options based on what is actually being proxied. Most notably the
// HTTP proxying connection creation is not something we currently control. // HTTP proxying connection creation is not something we currently control.
, net: require('net') , net: require('net')
}; };
deps.storage = require('./storage').create(deps, conf);
deps.proxy = require('./proxy-conn').create(deps, conf); modules = {
deps.socks5 = require('./socks5-server').create(deps, conf); storage: require('./storage').create(deps, conf)
deps.loopback = require('./loopback').create(deps, conf); , proxy: require('./proxy-conn').create(deps, conf)
deps.ddns = require('./ddns').create(deps, conf); , socks5: require('./socks5-server').create(deps, conf)
, ddns: require('./ddns').create(deps, conf)
};
Object.assign(deps, modules);
require('./goldilocks.js').create(deps, conf); require('./goldilocks.js').create(deps, conf);
process.removeListener('message', create); process.removeListener('message', create);

75
package-lock.json generated
View File

@ -427,6 +427,11 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
}, },
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
},
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1029,6 +1034,11 @@
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
}, },
"jsonschema": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.0.tgz",
"integrity": "sha512-XDJApzBauMg0TinJNP4iVcJl99PQ4JbWKK7nwzpOIkAOVveDKgh/2xm41T3x7Spu4PWMhnnQpNJmUSIUgl6sKg=="
},
"jsonwebtoken": { "jsonwebtoken": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz",
@ -1168,6 +1178,11 @@
"resolved": "https://registry.npmjs.org/localhost.daplie.me-certificates/-/localhost.daplie.me-certificates-1.3.5.tgz", "resolved": "https://registry.npmjs.org/localhost.daplie.me-certificates/-/localhost.daplie.me-certificates-1.3.5.tgz",
"integrity": "sha1-GjqH5PlX8mn2LP7mCmNpe9JVOpo=" "integrity": "sha1-GjqH5PlX8mn2LP7mCmNpe9JVOpo="
}, },
"lodash": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
},
"lodash.isplainobject": { "lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@ -1301,11 +1316,38 @@
"integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=",
"optional": true "optional": true
}, },
"needle": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/needle/-/needle-1.1.2.tgz",
"integrity": "sha1-0oQaElv9dP77MMA0QQQ2kGHD4To=",
"requires": {
"debug": "2.6.1",
"iconv-lite": "0.4.15"
}
},
"negotiator": { "negotiator": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
}, },
"network": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/network/-/network-0.4.0.tgz",
"integrity": "sha1-ngk+TZzpBjmHJTL6YC/oVf87aSk=",
"requires": {
"async": "1.5.2",
"commander": "2.9.0",
"needle": "1.1.2",
"wmic": "0.1.0"
},
"dependencies": {
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
}
}
},
"node-forge": { "node-forge": {
"version": "0.6.49", "version": "0.6.49",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.49.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.49.tgz",
@ -1865,9 +1907,9 @@
} }
}, },
"socket-pair": { "socket-pair": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/socket-pair/-/socket-pair-1.0.1.tgz", "resolved": "https://registry.npmjs.org/socket-pair/-/socket-pair-1.0.3.tgz",
"integrity": "sha1-mneFcEv9yOj2NxwodeyjIeMT/po=", "integrity": "sha512-O1WJMNIPAAGCzzJi1Lk9K9adctKM4DukiUO6G6sQSs+CqEAZ5uGX86uIMDKygBZZr62YHDoOGH1rJShOzw6i9Q==",
"requires": { "requires": {
"bluebird": "3.5.0" "bluebird": "3.5.0"
} }
@ -1967,14 +2009,6 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
}, },
"stream-pair": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-pair/-/stream-pair-1.0.3.tgz",
"integrity": "sha1-vIdY/jnTgQuva3VMj5BI8PuRNn0=",
"requires": {
"readable-stream": "2.2.11"
}
},
"string_decoder": { "string_decoder": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.2.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.2.tgz",
@ -2233,6 +2267,25 @@
} }
} }
}, },
"wmic": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/wmic/-/wmic-0.1.0.tgz",
"integrity": "sha1-eLQasR0VTLgSgZ4SkWdNrVXY4dc=",
"requires": {
"async": "2.5.0",
"iconv-lite": "0.4.15"
},
"dependencies": {
"async": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
"integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
"requires": {
"lodash": "4.17.4"
}
}
}
},
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@ -41,6 +41,7 @@
"bluebird": "^3.4.6", "bluebird": "^3.4.6",
"body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1",
"commander": "^2.9.0", "commander": "^2.9.0",
"deep-equal": "^1.0.1",
"dns-suite": "git+https://git@git.daplie.com/Daplie/dns-suite#v1", "dns-suite": "git+https://git@git.daplie.com/Daplie/dns-suite#v1",
"express": "git+https://github.com/expressjs/express.git#4.x", "express": "git+https://github.com/expressjs/express.git#4.x",
"finalhandler": "^0.4.0", "finalhandler": "^0.4.0",
@ -49,12 +50,14 @@
"human-readable-ids": "git+https://git.daplie.com/Daplie/human-readable-ids-js#master", "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", "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0",
"js-yaml": "^3.8.3", "js-yaml": "^3.8.3",
"jsonschema": "^1.2.0",
"jsonwebtoken": "^7.4.0", "jsonwebtoken": "^7.4.0",
"le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", "le-challenge-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": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master",
"le-challenge-sni": "^2.0.1", "le-challenge-sni": "^2.0.1",
"le-store-certbot": "git+https://git.daplie.com/Daplie/le-store-certbot.git#master", "le-store-certbot": "git+https://git.daplie.com/Daplie/le-store-certbot.git#master",
"localhost.daplie.me-certificates": "^1.3.5", "localhost.daplie.me-certificates": "^1.3.5",
"network": "^0.4.0",
"recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4", "recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4",
"redirect-https": "^1.1.0", "redirect-https": "^1.1.0",
"request": "^2.81.0", "request": "^2.81.0",
@ -63,7 +66,7 @@
"serve-static": "^1.10.0", "serve-static": "^1.10.0",
"server-destroy": "^1.0.1", "server-destroy": "^1.0.1",
"sni": "^1.0.0", "sni": "^1.0.0",
"socket-pair": "^1.0.1", "socket-pair": "^1.0.3",
"socksv5": "0.0.6", "socksv5": "0.0.6",
"stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1", "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1",
"stunneld": "git+https://git.daplie.com/Daplie/node-tunnel-server.git#v1", "stunneld": "git+https://git.daplie.com/Daplie/node-tunnel-server.git#v1",

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')
);