Compare commits
	
		
			No commits in common. "master" and "tunnel" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,7 +1,6 @@ | |||||||
| *session* | *session* | ||||||
| *secret* | *secret* | ||||||
| var/* | var/* | ||||||
| packages/assets/org.oauth3 |  | ||||||
| 
 | 
 | ||||||
| # Logs | # Logs | ||||||
| logs | logs | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | [submodule "packages/assets/org.oauth3"] | ||||||
|  | 	path = packages/assets/org.oauth3 | ||||||
|  | 	url = git@git.daplie.com:OAuth3/oauth3.js.git | ||||||
							
								
								
									
										17
									
								
								.jshintrc
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								.jshintrc
									
									
									
									
									
								
							| @ -1,17 +0,0 @@ | |||||||
| { "node": true |  | ||||||
| , "browser": true |  | ||||||
| , "jquery": true |  | ||||||
| , "strict": true |  | ||||||
| , "indent": 2 |  | ||||||
| , "onevar": true |  | ||||||
| , "laxcomma": true |  | ||||||
| , "laxbreak": true |  | ||||||
| , "eqeqeq": true |  | ||||||
| , "immed": true |  | ||||||
| , "undef": true |  | ||||||
| , "unused": true |  | ||||||
| , "latedef": true |  | ||||||
| , "curly": true |  | ||||||
| , "trailing": true |  | ||||||
| , "esversion": 6 |  | ||||||
| } |  | ||||||
							
								
								
									
										171
									
								
								API.md
									
									
									
									
									
								
							
							
						
						
									
										171
									
								
								API.md
									
									
									
									
									
								
							| @ -1,171 +0,0 @@ | |||||||
| # API |  | ||||||
| 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: |  | ||||||
| 
 |  | ||||||
| * localhost.alpha.daplie.me |  | ||||||
| * localhost.admin.daplie.me |  | ||||||
| * alpha.localhost.daplie.me |  | ||||||
| * admin.localhost.daplie.me |  | ||||||
| * localhost.daplie.invalid |  | ||||||
| 
 |  | ||||||
| All requests require an OAuth3 token in the request headers. |  | ||||||
| 
 |  | ||||||
| ## Tokens |  | ||||||
| 
 |  | ||||||
| Some of the functionality of goldilocks requires the use of OAuth3 tokens to |  | ||||||
| perform tasks like setting DNS records. Management of these tokens can be done |  | ||||||
| using the following APIs. |  | ||||||
| 
 |  | ||||||
| ### Get A Single Token |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/tokens/:id` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: The token matching the specified ID. Has the following properties. |  | ||||||
|     * `id`: The hash used to identify the token. Based on several of the fields |  | ||||||
|       inside the decoded token. |  | ||||||
|     * `provider_uri`: The URI for the one who issued the token. Should be the same |  | ||||||
|       as the `iss` field inside the decoded token. |  | ||||||
|     * `client_uri`: The URI for the app authorized to use the token. Should be the |  | ||||||
|       same as the `azp` field inside the decoded token. |  | ||||||
|     * `scope`: The list of permissions granted by the token. Should be the same |  | ||||||
|       as the `scp` field inside the decoded token. |  | ||||||
|     * `access_token`: The encoded JWT. |  | ||||||
|     * `token`: The decoded token. |  | ||||||
| 
 |  | ||||||
| ### Get All Tokens |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/tokens` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: An array of the tokens stored. Each item looks the same as if it |  | ||||||
|     had been requested individually. |  | ||||||
| 
 |  | ||||||
| ### Save New Token |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/tokens` |  | ||||||
|   * **Method** `POST` |  | ||||||
|   * **Body**: An object similar to an OAuth3 session used by the javascript |  | ||||||
|     library. The only important fields are `refresh_token` or `access_token`, and |  | ||||||
|     `refresh_token` will be used before `access_token`. (This is because the |  | ||||||
|     `access_token` usually expires quickly, making it meaningless to store.) |  | ||||||
|   * **Reponse**: The response looks the same as a single GET request. |  | ||||||
| 
 |  | ||||||
| ### Delete Token |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/tokens/:id` |  | ||||||
|   * **Method** `DELETE` |  | ||||||
|   * **Reponse**: Either `{"success":true}` or `{"success":false}`, depending on |  | ||||||
|     whether the token was present before the request. |  | ||||||
| 
 |  | ||||||
| ## Config |  | ||||||
| 
 |  | ||||||
| ### Get All Settings |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: The JSON representation of the current config. See the [README.md](/README.md) |  | ||||||
|     for the structure of the config. |  | ||||||
| 
 |  | ||||||
| ### Get Group Setting |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/:group` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: The sub-object of the config relevant to the group specified in |  | ||||||
|     the url (ie http, tls, tcp, etc.) |  | ||||||
| 
 |  | ||||||
| ### Get Group Module List |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/:group/modules` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: The list of modules relevant to the group specified in the url |  | ||||||
|     (ie http, tls, tcp, etc.) |  | ||||||
| 
 |  | ||||||
| ### Get Specific Module |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/:group/modules/:modId` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: The module with the specified module ID. |  | ||||||
| 
 |  | ||||||
| ### Get Domain Group |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: The domains specification with the specified domains ID. |  | ||||||
| 
 |  | ||||||
| ### Get Domain Group Modules |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: An object containing all of the relevant modules for the group |  | ||||||
|     of domains. |  | ||||||
| 
 |  | ||||||
| ### Get Domain Group Module Category |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: A list of the specific category of modules for the group of domains. |  | ||||||
| 
 |  | ||||||
| ### Get Specific Domain Group Module |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group/:modId` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Reponse**: The module with the specified module ID. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ### Change Settings |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config` |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/:group` |  | ||||||
|   * **Method** `POST` |  | ||||||
|   * **Body**: The changes to be applied on top of the current config. See the |  | ||||||
|     [README.md](/README.md) for the settings. If modules or domains are specified |  | ||||||
|     they are added to the current list. |  | ||||||
|   * **Reponse**: The current config. If the group is specified in the URL it will |  | ||||||
|     only be the config relevant to that group. |  | ||||||
| 
 |  | ||||||
| ### Add Module |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/:group/modules` |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group` |  | ||||||
|   * **Method** `POST` |  | ||||||
|   * **Body**: The module to be added. Can also be provided an array of modules |  | ||||||
|     to add multiple modules in the same request. |  | ||||||
|   * **Reponse**: The current list of modules. |  | ||||||
| 
 |  | ||||||
| ### Add Domain Group |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains` |  | ||||||
|   * **Method** `POST` |  | ||||||
|   * **Body**: The domains names and modules for the new domain group(s). |  | ||||||
|   * **Reponse**: The current list of domain groups. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ### Edit Module |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/:group/modules/:modId` |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group/:modId` |  | ||||||
|   * **Method** `PUT` |  | ||||||
|   * **Body**: The new parameters for the module. |  | ||||||
|   * **Reponse**: The editted module. |  | ||||||
| 
 |  | ||||||
| ### Edit Domain Group |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId` |  | ||||||
|   * **Method** `PUT` |  | ||||||
|   * **Body**: The new domains names for the domains group. The module list cannot |  | ||||||
|     be editted through this route. |  | ||||||
|   * **Reponse**: The editted domain group. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ### Remove Module |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/:group/modules/:modId` |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId/modules/:group/:modId` |  | ||||||
|   * **Method** `DELETE` |  | ||||||
|   * **Reponse**: The list of modules. |  | ||||||
| 
 |  | ||||||
| ### Remove Domain Group |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/config/domains/:domId` |  | ||||||
|   * **Method** `DELETE` |  | ||||||
|   * **Reponse**: The list of domain groups. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Socks5 Proxy |  | ||||||
| 
 |  | ||||||
| ### Check Status |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/socks5` |  | ||||||
|   * **Method** `GET` |  | ||||||
|   * **Response**: The returned object will have up to two values inside |  | ||||||
|     * `running`: boolean value to indicate if the proxy is currently active |  | ||||||
|     * `port`: if the proxy is running this is the port it's running on |  | ||||||
| 
 |  | ||||||
| ### Start Proxy |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/socks5` |  | ||||||
|   * **Method** `POST` |  | ||||||
|   * **Response**: Same response as for the `GET` request |  | ||||||
| 
 |  | ||||||
| ### Stop Proxy |  | ||||||
|   * **URL** `/api/goldilocks@daplie.com/socks5` |  | ||||||
|   * **Method** `DELETE` |  | ||||||
|   * **Response**: Same response as for the `GET` request |  | ||||||
							
								
								
									
										12
									
								
								CHANGELOG
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								CHANGELOG
									
									
									
									
									
								
							| @ -1,12 +0,0 @@ | |||||||
| v1.1.5 - Implemented dns-01 ACME challenges |  | ||||||
| 
 |  | ||||||
| v1.1.4 - Improved responsiveness to config updates |  | ||||||
|   * changed which TCP/UDP ports are bound to on config update |  | ||||||
|   * update tunnel server settings on config update |  | ||||||
|   * update socks5 setting on config update |  | ||||||
| 
 |  | ||||||
| v1.1.3 - Better late than never... here's some stuff we've got |  | ||||||
|   * fixed (probably) network settings not being readable |  | ||||||
|   * supports timeouts in loopback check |  | ||||||
|   * loopback check less likely to fail / throw errors, will try again |  | ||||||
|   * supports ddns using audience of token |  | ||||||
							
								
								
									
										41
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								LICENSE
									
									
									
									
									
								
							| @ -1,41 +0,0 @@ | |||||||
| Copyright 2017 Daplie, Inc |  | ||||||
| 
 |  | ||||||
| This is open source software; you can redistribute it and/or modify it under the |  | ||||||
| terms of either: |  | ||||||
| 
 |  | ||||||
|    a) the "MIT License" |  | ||||||
|    b) the "Apache-2.0 License" |  | ||||||
| 
 |  | ||||||
| MIT License |  | ||||||
| 
 |  | ||||||
|    Permission is hereby granted, free of charge, to any person obtaining a copy |  | ||||||
|    of this software and associated documentation files (the "Software"), to deal |  | ||||||
|    in the Software without restriction, including without limitation the rights |  | ||||||
|    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |  | ||||||
|    copies of the Software, and to permit persons to whom the Software is |  | ||||||
|    furnished to do so, subject to the following conditions: |  | ||||||
| 
 |  | ||||||
|    The above copyright notice and this permission notice shall be included in all |  | ||||||
|    copies or substantial portions of the Software. |  | ||||||
| 
 |  | ||||||
|    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |  | ||||||
|    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |  | ||||||
|    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |  | ||||||
|    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |  | ||||||
|    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |  | ||||||
|    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |  | ||||||
|    SOFTWARE. |  | ||||||
| 
 |  | ||||||
| Apache-2.0 License Summary |  | ||||||
| 
 |  | ||||||
|    Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
|    you may not use this file except in compliance with the License. |  | ||||||
|    You may obtain a copy of the License at |  | ||||||
| 
 |  | ||||||
|      http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
| 
 |  | ||||||
|    Unless required by applicable law or agreed to in writing, software |  | ||||||
|    distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
|    See the License for the specific language governing permissions and |  | ||||||
|    limitations under the License. |  | ||||||
							
								
								
									
										3
									
								
								LICENSE.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								LICENSE.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | Hello all. We make our source code available to view, but we retain copyright. | ||||||
|  | 
 | ||||||
|  | It's not because we're trying to be mean or anything, we just want to maintain our distribution channel. | ||||||
							
								
								
									
										720
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										720
									
								
								README.md
									
									
									
									
									
								
							| @ -1,70 +1,45 @@ | |||||||
|  | <!-- BANNER_TPL_BEGIN --> | ||||||
|  | 
 | ||||||
|  | About Daplie: We're taking back the Internet! | ||||||
|  | -------------- | ||||||
|  | 
 | ||||||
|  | Down with Google, Apple, and Facebook! | ||||||
|  | 
 | ||||||
|  | We're re-decentralizing the web and making it read-write again - one home cloud system at a time. | ||||||
|  | 
 | ||||||
|  | Tired of serving the Empire? Come join the Rebel Alliance: | ||||||
|  | 
 | ||||||
|  | <a href="mailto:jobs@daplie.com">jobs@daplie.com</a> | [Invest in Daplie on Wefunder](https://daplie.com/invest/) | [Pre-order Cloud](https://daplie.com/preorder/), The World's First Home Server for Everyone | ||||||
|  | 
 | ||||||
|  | <!-- BANNER_TPL_END --> | ||||||
|  | 
 | ||||||
| Goldilocks | Goldilocks | ||||||
| ========== | ========== | ||||||
| 
 | 
 | ||||||
| The node.js netserver that's just right. | The node.js webserver that's just right. | ||||||
| 
 | 
 | ||||||
| * **HTTPS Web Server** with Automatic TLS (SSL) via ACME ([Let's Encrypt](https://letsencrypt.org)) |  | ||||||
|   * Static Web Server |  | ||||||
|   * URL Redirects |  | ||||||
|   * SSL on localhost (with bundled localhost.daplie.me certificates) |  | ||||||
|   * Uses node cluster to take advantage of multiple CPUs (in progress) |  | ||||||
| * **TLS** name-based (SNI) proxy |  | ||||||
| * **TCP** port-based proxy |  | ||||||
| * WS **Tunnel Server** (i.e. run on Digital Ocean and expose a home-firewalled Raspberry Pi to the Internet) |  | ||||||
| * WS **Tunnel Client** (i.e. run on a Raspberry Pi and connect to a Daplie Tunnel) |  | ||||||
| * UPnP / NAT-PMP forwarding and loopback testing (in progress) |  | ||||||
| * Configurable via API |  | ||||||
| * mDNS Discoverable (configure in home or office with mobile and desktop apps) |  | ||||||
| * OAuth3 Authentication |  | ||||||
| 
 | 
 | ||||||
| Install Standalone | A simple HTTPS static file server with valid TLS (SSL) certs. | ||||||
|  | 
 | ||||||
|  | Comes bundled a valid certificate for localhost.daplie.me, | ||||||
|  | which is great for testing and development, and you can specify your own. | ||||||
|  | 
 | ||||||
|  | Also great for testing ACME certs from letsencrypt.org. | ||||||
|  | 
 | ||||||
|  | Install | ||||||
| ------- | ------- | ||||||
| 
 | 
 | ||||||
| ### curl | bash |  | ||||||
| 
 |  | ||||||
| ```bash | ```bash | ||||||
| curl -fsSL https://git.coolaj86.com/coolaj86/goldilocks.js/raw/v1.1/installer/get.sh | bash | # v2 in npm | ||||||
|  | npm install -g goldilocks | ||||||
|  | 
 | ||||||
|  | # master in git (via ssh) | ||||||
|  | npm install -g git+ssh://git@git.daplie.com:Daplie/goldilocks.js | ||||||
|  | 
 | ||||||
|  | # master in git (unauthenticated) | ||||||
|  | npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### git |  | ||||||
| 
 |  | ||||||
| ```bash |  | ||||||
| git clone https://git.coolaj86.com/coolaj86/goldilocks.js |  | ||||||
| pushd goldilocks.js |  | ||||||
| git checkout v1.1 |  | ||||||
| bash installer/install.sh |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### npm |  | ||||||
| 
 |  | ||||||
| ```bash |  | ||||||
| # v1 in git (unauthenticated) |  | ||||||
| npm install -g git+https://git@git.coolaj86.com:coolaj86/goldilocks.js#v1 |  | ||||||
| 
 |  | ||||||
| # v1 in git (via ssh) |  | ||||||
| npm install -g git+ssh://git@git.coolaj86.com:coolaj86/goldilocks.js#v1 |  | ||||||
| 
 |  | ||||||
| # v1 in npm |  | ||||||
| npm install -g goldilocks@v1 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### Uninstall |  | ||||||
| 
 |  | ||||||
| Remove goldilocks and services: |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| rm -rf /opt/goldilocks/ /srv/goldilocks/ /var/goldilocks/ /var/log/goldilocks/ /etc/tmpfiles.d/goldilocks.conf /etc/systemd/system/goldilocks.service |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Remove config as well |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| rm -rf /etc/goldilocks/ /etc/ssl/goldilocks |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Usage |  | ||||||
| ----- |  | ||||||
| 
 |  | ||||||
| ```bash | ```bash | ||||||
| goldilocks | goldilocks | ||||||
| ``` | ``` | ||||||
| @ -73,581 +48,114 @@ goldilocks | |||||||
| Serving /Users/foo/ at https://localhost.daplie.me:8443 | Serving /Users/foo/ at https://localhost.daplie.me:8443 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Install as a System Service (daemon-mode) | Usage | ||||||
| 
 |  | ||||||
| We have service support for |  | ||||||
| 
 |  | ||||||
| * systemd (Linux, Ubuntu) |  | ||||||
| * launchd (macOS) |  | ||||||
| 
 |  | ||||||
| ```bash |  | ||||||
| curl https://git.coolaj86.com/coolaj86/goldilocks.js/raw/master/install.sh | bash |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Modules & Configuration |  | ||||||
| ----- | ----- | ||||||
| 
 | 
 | ||||||
| Goldilocks has several core systems, which all have their own configuration and | Examples: | ||||||
| some of which have modules: |  | ||||||
| 
 |  | ||||||
| * [http](#http) |  | ||||||
|   - [proxy (reverse proxy)](#httpproxy-how-to-reverse-proxy-ruby-python-etc) |  | ||||||
|   - [static](#httpstatic-how-to-serve-a-web-page) |  | ||||||
|   - [redirect](#httpredirect-how-to-redirect-urls) |  | ||||||
| * [tls](#tls) |  | ||||||
|   - [proxy (reverse proxy)](#tlsproxy) |  | ||||||
|   - [acme](#tlsacme) |  | ||||||
| * [tcp](#tcp) |  | ||||||
|   - [proxy](#tcpproxy) |  | ||||||
|   - [forward](#tcpforward) |  | ||||||
| * [udp](#udp) |  | ||||||
|   - [forward](#udpforward) |  | ||||||
| * [domains](#domains) |  | ||||||
| * [tunnel_server](#tunnel_server) |  | ||||||
| * [DDNS](#ddns) |  | ||||||
| * [tunnel_client](#tunnel) |  | ||||||
| * [mDNS](#mdns) |  | ||||||
| * [socks5](#socks5) |  | ||||||
| * api |  | ||||||
| 
 |  | ||||||
| All modules require a `type` and an `id`, and any modules not defined inside the |  | ||||||
| `domains` system also require a `domains` field (with the exception of the `forward` |  | ||||||
| modules that require the `ports` field). |  | ||||||
| 
 |  | ||||||
| ### http |  | ||||||
| 
 |  | ||||||
| The HTTP system handles plain http (TLS / SSL is handled by the tls system) |  | ||||||
| 
 |  | ||||||
| Example config: |  | ||||||
| ```yml |  | ||||||
| http: |  | ||||||
|   trust_proxy: true                 # allow localhost, 192.x, 10.x, 172.x, etc to set headers |  | ||||||
|   allow_insecure: false             # allow non-https even without proxy https headers |  | ||||||
|   primary_domain: example.com       # attempts to access via IP address will redirect here |  | ||||||
| 
 |  | ||||||
|   # An array of modules that define how to handle incoming HTTP requests |  | ||||||
|   modules: |  | ||||||
|     - type: static |  | ||||||
|       domains: |  | ||||||
|         - example.com |  | ||||||
|       root: /srv/www/:hostname |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### http.proxy - how to reverse proxy (ruby, python, etc) |  | ||||||
| 
 |  | ||||||
| The proxy module is for reverse proxying, typically to an application on the same machine. |  | ||||||
| (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: |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - api.example.com |  | ||||||
|       host: 192.168.1.100 |  | ||||||
|       port: 80 |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - www.example.com |  | ||||||
|       address: 192.168.1.16:80 |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - '*' |  | ||||||
|       port: 3000 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### http.static - how to serve a web page |  | ||||||
| 
 |  | ||||||
| The static module is for serving static web pages and assets and has the following options: |  | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| root        The path to serve as a string. | # Install | ||||||
|             The template variable `:hostname` represents the HTTP Host header without port information | npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js | ||||||
|             ex: `root: /srv/www/example.com` would load the example.com folder for any domain listed |  | ||||||
|             ex: `root: /srv/www/:hostname` would load `/srv/www/example.com` if so indicated by the Host header |  | ||||||
| 
 | 
 | ||||||
| index       Set to `false` to disable the default behavior of loading `index.html` in directories | # Use tunnel | ||||||
|             ex: `false` | goldilocks --sites jane.daplie.me --agree-tos --email jane@example.com --tunnel | ||||||
| 
 | 
 | ||||||
| dotfiles    Set to `allow` to load dotfiles rather than ignoring them | # BEFORE you access in a browser for the first time, use curl | ||||||
|             ex: `"allow"` | # (because there's a concurrency bug in the greenlock setup) | ||||||
| 
 | curl https://jane.daplie.me | ||||||
| redirect    Set to `false` to disable the default behavior of ensuring that directory paths end in '/' |  | ||||||
|             ex: `false` |  | ||||||
| 
 |  | ||||||
| indexes     An array of directories which should be have indexes served rather than blocked |  | ||||||
|             ex: `[ '/' ]` will allow all directories indexes to be served |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Example config: | Options: | ||||||
| ```yml | 
 | ||||||
| http: | * `-p <port>` - i.e. `sudo goldilocks -p 443` (defaults to 80+443 or 8443) | ||||||
|   modules: | * `-d <dirpath>` - i.e. `goldilocks -d /tmp/` (defaults to `pwd`) | ||||||
|     - type: static |   * you can use `:hostname` as a template for multiple directories | ||||||
|       domains: |   * Example A: `goldilocks -d /srv/www/:hostname --sites localhost.foo.daplie.me,localhost.bar.daplie.me` | ||||||
|         - example.com |   * Example B: `goldilocks -d ./:hostname/public/ --sites localhost.foo.daplie.me,localhost.bar.daplie.me` | ||||||
|       root: /srv/www/:hostname | * `-c <content>` - i.e. `server-https -c 'Hello, World! '` (defaults to directory index) | ||||||
|  | * `--express-app <path>` - path to a file the exports an express-style app (`function (req, res, next) { ... }`) | ||||||
|  | * `--livereload` - inject livereload into all html pages (see also: [fswatch](http://stackoverflow.com/a/13807906/151312)), but be careful if `<dirpath>` has thousands of files it will spike your CPU usage to 100% | ||||||
|  | 
 | ||||||
|  | * `--email <email>` - email to use for Let's Encrypt, Daplie DNS, Daplie Tunnel | ||||||
|  | * `--agree-tos` - agree to terms for Let's Encrypt, Daplie DNS | ||||||
|  | * `--sites <domain.tld>` comma-separated list of domains to respond to (default is `localhost.daplie.me`) | ||||||
|  |   * optionally you may include the path to serve with `|` such as `example.com|/tmp,example.net/srv/www` | ||||||
|  | * `--tunnel` - make world-visible (must use `--sites`) | ||||||
|  | 
 | ||||||
|  | Specifying a custom HTTPS certificate: | ||||||
|  | 
 | ||||||
|  | * `--key /path/to/privkey.pem` specifies the server private key | ||||||
|  | * `--cert /path/to/fullchain.pem` specifies the bundle of server certificate and all intermediate certificates | ||||||
|  | * `--root /path/to/root.pem` specifies the certificate authority(ies) | ||||||
|  | 
 | ||||||
|  | Note: `--root` may specify single cert or a bundle, and may be used multiple times like so: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | --root /path/to/primary-root.pem --root /path/to/cross-root.pem | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### http.redirect - how to redirect URLs | Other options: | ||||||
| 
 | 
 | ||||||
| The redirect module is for, you guessed it, redirecting URLs. | * `--serve-root true` alias for `-c` with the contents of root.pem | ||||||
|  | * `--sites example.com` changes the servername logged to the console | ||||||
|  | * `--letsencrypt-certs example.com` sets and key, fullchain, and root to standard letsencrypt locations | ||||||
| 
 | 
 | ||||||
| It has the following options: | Examples | ||||||
| ``` | -------- | ||||||
| status      The HTTP status code to issue (301 is usual permanent redirect, 302 is temporary) |  | ||||||
|             ex: 301 |  | ||||||
| 
 |  | ||||||
| from        The URL path that was used in the request. |  | ||||||
|             The `*` wildcard character can be used for matching a full segment of the path |  | ||||||
|             ex: /photos/ |  | ||||||
|             ex: /photos/*/*/ |  | ||||||
| 
 |  | ||||||
| to          The new URL path which should be used. |  | ||||||
|             If wildcards matches were used they will be available as `:1`, `:2`, etc. |  | ||||||
|             ex: /pics/ |  | ||||||
|             ex: /pics/:1/:2/ |  | ||||||
|             ex: https://mydomain.com/photos/:1/:2/ |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Example config: |  | ||||||
| ```yml |  | ||||||
| http: |  | ||||||
|   modules: |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - example.com |  | ||||||
|       status: 301 |  | ||||||
|       from: /archives/*/*/*/ |  | ||||||
|       to: https://example.net/year/:1/month/:2/day/:3/ |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### tls |  | ||||||
| 
 |  | ||||||
| The tls system handles encrypted connections, including fetching certificates, |  | ||||||
| and uses ServerName Indication (SNI) to determine if the connection should be |  | ||||||
| handled by the http system, a tls system module, or rejected. |  | ||||||
| 
 |  | ||||||
| Example config: |  | ||||||
| ```yml |  | ||||||
| tls: |  | ||||||
|   modules: |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - example.com |  | ||||||
|         - example.net |  | ||||||
|       address: '127.0.0.1:6443' |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Certificates are saved to `~/acme`, which may be `/var/www/acme` if Goldilocks is run as the www-data user. |  | ||||||
| 
 |  | ||||||
| ### tls.proxy |  | ||||||
| 
 |  | ||||||
| The proxy module routes the traffic based on the ServerName Indication (SNI) **without decrypting** it. |  | ||||||
| 
 |  | ||||||
| It has the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc). |  | ||||||
| 
 |  | ||||||
| Example config: |  | ||||||
| ```yml |  | ||||||
| tls: |  | ||||||
|   modules: |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - example.com |  | ||||||
|       address: '127.0.0.1:5443' |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### tls.acme |  | ||||||
| 
 |  | ||||||
| The acme module defines the setting used when getting new certificates. |  | ||||||
| 
 |  | ||||||
| It has the following options: |  | ||||||
| ``` |  | ||||||
| email              The email address for ACME certificate issuance |  | ||||||
|                    ex: john.doe@example.com |  | ||||||
| 
 |  | ||||||
| server             The ACME server to use |  | ||||||
|                    ex: https://acme-v01.api.letsencrypt.org/directory |  | ||||||
|                    ex: https://acme-staging.api.letsencrypt.org/directory |  | ||||||
| 
 |  | ||||||
| challenge_type     The ACME challenge to request |  | ||||||
|                    ex: http-01, dns-01, tls-01 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Example config: |  | ||||||
| ```yml |  | ||||||
| tls: |  | ||||||
|   modules: |  | ||||||
|     - type: acme |  | ||||||
|       domains: |  | ||||||
|         - example.com |  | ||||||
|         - example.net |  | ||||||
|       email: 'joe.shmoe@example.com' |  | ||||||
|       server: 'https://acme-staging.api.letsencrypt.org/directory' |  | ||||||
|       challenge_type: 'http-01' |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| **NOTE:** If you specify `dns-01` as the challenge type there must also be a |  | ||||||
| [DDNS module](#ddns) defined for all of the relevant domains (though not all |  | ||||||
| domains handled by a single TLS module need to be handled by the same DDNS |  | ||||||
| module). The DDNS module provides all of the information needed to actually |  | ||||||
| set the DNS records needed to verify ownership. |  | ||||||
| 
 |  | ||||||
| ### tcp |  | ||||||
| 
 |  | ||||||
| The tcp system handles both *raw* and *tls-terminated* tcp network traffic |  | ||||||
| (see the _Note_ section below the example). It may use port numbers |  | ||||||
| or traffic sniffing to determine how the connection should be handled. |  | ||||||
| 
 |  | ||||||
| It has the following options: |  | ||||||
| ``` |  | ||||||
| bind      An array of numeric ports on which to bind |  | ||||||
|           ex: 80 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Example Config: |  | ||||||
| ```yml |  | ||||||
| tcp: |  | ||||||
|   bind: |  | ||||||
|     - 22 |  | ||||||
|     - 80 |  | ||||||
|     - 443 |  | ||||||
|   modules: |  | ||||||
|     - type: forward |  | ||||||
|       ports: |  | ||||||
|         - 22 |  | ||||||
|       address: '127.0.0.1:2222' |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| _Note_: When tcp traffic comes into goldilocks it will be tested against the tcp modules. |  | ||||||
| The connection may be handed to the TLS module if it appears to be a TLS/SSL/HTTPS connection |  | ||||||
| and if the tls module terminates the traffic, the connection will be sent back to the TLS module. |  | ||||||
| Due to the complexity of node.js' networking stack it is not currently possible to tell which |  | ||||||
| port tls-terminated traffic came from, so only the SNI header (serername / domain name) may be used for |  | ||||||
| modules matching terminated TLS. |  | ||||||
| 
 |  | ||||||
| ### tcp.proxy |  | ||||||
| 
 |  | ||||||
| The proxy module routes traffic **after tls-termination** based on the servername (domain name) |  | ||||||
| contained in a SNI header. As such this only works to route TCP connections wrapped in a TLS stream. |  | ||||||
| 
 |  | ||||||
| It has the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc). |  | ||||||
| 
 |  | ||||||
| This is particularly useful for routing ssh and vpn traffic over tcp port 443 as wrapped TLS |  | ||||||
| connections in order to access one of your servers even when connecting from a harsh or potentially |  | ||||||
| misconfigured network environment (i.e. hotspots in public libraries and shopping malls). |  | ||||||
| 
 |  | ||||||
| Example config: |  | ||||||
| ```yml |  | ||||||
| tcp: |  | ||||||
|   modules: |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - ssh.example.com      # Note: this domain would also listed in tls.acme.domains |  | ||||||
|       host: localhost |  | ||||||
|       port: 22 |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - vpn.example.com      # Note: this domain would also listed in tls.acme.domains |  | ||||||
|       host: localhost |  | ||||||
|       port: 1194 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| _Note_: In same cases network administrators purposefully block ssh and vpn connections using |  | ||||||
| Application Firewalls with DPI (deep packet inspection) enabled. You should read the ToS of the |  | ||||||
| network you are connected to to ensure that you aren't subverting policies that are purposefully |  | ||||||
| in place on such networks. |  | ||||||
| 
 |  | ||||||
| #### Using with ssh |  | ||||||
| 
 |  | ||||||
| In order to use this to route SSH connections you will need to use `ssh`'s |  | ||||||
| `ProxyCommand` option. For example to use the TLS certificate for `ssh.example.com` |  | ||||||
| to wrap an ssh connection you could use the following command: |  | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| ssh user@example.com -o ProxyCommand='openssl s_client -quiet -connect example.com:443 -servername ssh.example.com' | goldilocks -p 1443 -c 'Hello from 1443' & | ||||||
|  | goldilocks -p 2443 -c 'Hello from 2443' & | ||||||
|  | goldilocks -p 3443 -d /tmp & | ||||||
|  | 
 | ||||||
|  | curl https://localhost.daplie.me:1443 | ||||||
|  | > Hello from 1443 | ||||||
|  | 
 | ||||||
|  | curl --insecure https://localhost:2443 | ||||||
|  | > Hello from 2443 | ||||||
|  | 
 | ||||||
|  | curl https://localhost.daplie.me:3443 | ||||||
|  | > [html index listing of /tmp] | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Alternatively you could add the following lines to your ssh config file. | And if you tested <http://localhost.daplie.me:3443> in a browser, | ||||||
| ``` | it would redirect to <https://localhost.daplie.me:3443> (on the same port). | ||||||
| Host example.com |  | ||||||
|   ProxyCommand openssl s_client -quiet -connect example.com:443 -servername ssh.example.com |  | ||||||
| ``` |  | ||||||
| 
 | 
 | ||||||
| #### Using with OpenVPN | (in curl it would just show an error message) | ||||||
| 
 | 
 | ||||||
| There are two strategies that will work well for you: | ### Testing ACME Let's Encrypt certs | ||||||
| 
 | 
 | ||||||
| 1) [Use ssh](https://redfern.me/tunneling-openvpn-through-ssh/) with the config above to reverse proxy tcp port 1194 to you. | In case you didn't know, you can get free https certificates from | ||||||
|  | [letsencrypt.org](https://letsencrypt.org) | ||||||
|  | (ACME letsencrypt) | ||||||
|  | and even a free subdomain from <https://freedns.afraid.org>. | ||||||
|  | 
 | ||||||
|  | If you want to quickly test the certificates you installed, | ||||||
|  | you can do so like this: | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| ssh -L 1194:localhost:1194 example.com | goldilocks -p 8443 \ | ||||||
|  |   --letsencrypt-certs test.mooo.com \ | ||||||
|  |   --serve-root true | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| 2) [Use stunnel]https://serverfault.com/questions/675553/stunnel-vpn-traffic-and-ensure-it-looks-like-ssl-traffic-on-port-443/681497) | which is equilavent to | ||||||
| 
 | 
 | ||||||
| ``` | ```bash | ||||||
| [openvpn-over-goldilocks] | goldilocks -p 8443 \ | ||||||
| client = yes |   --sites test.mooo.com | ||||||
| accept = 127.0.0.1:1194 |   --key /etc/letsencrypt/live/test.mooo.com/privkey.pem \ | ||||||
| sni = vpn.example.com |   --cert /etc/letsencrypt/live/test.mooo.com/fullchain.pem \ | ||||||
| connect = example.com:443 |   --root /etc/letsencrypt/live/test.mooo.com/root.pem \ | ||||||
|  |   -c "$(cat 'sudo /etc/letsencrypt/live/test.mooo.com/root.pem')" | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| 3) [Use stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js) as described in the "tunnel_server" section below. | and can be tested like so | ||||||
| 
 | 
 | ||||||
| ### tcp.forward | ```bash | ||||||
| 
 | curl --insecure https://test.mooo.com:8443 > ./root.pem | ||||||
| The forward module routes traffic based on port number **without decrypting** it. | curl https://test.mooo.com:8843 --cacert ./root.pem | ||||||
| 
 |  | ||||||
| In addition to the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc), |  | ||||||
| the TCP forward modules also has the following options: |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| ports       A numeric array of source ports |  | ||||||
|             ex: 22 |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Example Config: | * [QuickStart Guide for Let's Encrypt](https://coolaj86.com/articles/lets-encrypt-on-raspberry-pi/) | ||||||
| ```yml | * [QuickStart Guide for FreeDNS](https://coolaj86.com/articles/free-dns-hosting-with-freedns-afraid-org.html) | ||||||
| tcp: |  | ||||||
|   bind: |  | ||||||
|     - 22 |  | ||||||
|     - 80 |  | ||||||
|     - 443 |  | ||||||
|   modules: |  | ||||||
|     - type: forward |  | ||||||
|       ports: |  | ||||||
|         - 22 |  | ||||||
|       port: 2222 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### udp |  | ||||||
| 
 |  | ||||||
| The udp system handles all udp network traffic. It currently only supports |  | ||||||
| forwarding the messages without any examination. |  | ||||||
| 
 |  | ||||||
| It has the following options: |  | ||||||
| ``` |  | ||||||
| bind      An array of numeric ports on which to bind |  | ||||||
|           ex: 53 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Example Config: |  | ||||||
| ```yml |  | ||||||
| udp: |  | ||||||
|   bind: |  | ||||||
|     - 53 |  | ||||||
|   modules: |  | ||||||
|     - type: forward |  | ||||||
|       ports: |  | ||||||
|         - 53 |  | ||||||
|       address: '127.0.0.1:8053' |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### udp.forward |  | ||||||
| 
 |  | ||||||
| The forward module routes traffic based on port number **without decrypting** it. |  | ||||||
| 
 |  | ||||||
| It has the same options as the [TCP forward module](#tcpforward). |  | ||||||
| 
 |  | ||||||
| Example Config: |  | ||||||
| ```yml |  | ||||||
| udp: |  | ||||||
|   bind: |  | ||||||
|     - 53 |  | ||||||
|   modules: |  | ||||||
|     - type: forward |  | ||||||
|       ports: |  | ||||||
|         - 53 |  | ||||||
|       address: '127.0.0.1:8053' |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### domains |  | ||||||
| 
 |  | ||||||
| To reduce repetition defining multiple modules that operate on the same domain |  | ||||||
| name the `domains` field can define multiple modules of multiple types for a |  | ||||||
| single list of names. The modules defined this way do not need to have their |  | ||||||
| own `domains` field. Note that the [tcp.forward](#tcpforward) module is not |  | ||||||
| allowed in a domains group since its routing is not based on domains. |  | ||||||
| 
 |  | ||||||
| Example Config |  | ||||||
| 
 |  | ||||||
| ```yml |  | ||||||
| domains: |  | ||||||
|   - names: |  | ||||||
|       - example.com |  | ||||||
|       - www.example.com |  | ||||||
|       - api.example.com |  | ||||||
|     modules: |  | ||||||
|       tls: |  | ||||||
|         - type: acme |  | ||||||
|           email: joe.schmoe@example.com |  | ||||||
|           challenge_type: 'http-01' |  | ||||||
|       http: |  | ||||||
|         - type: redirect |  | ||||||
|           from: /deprecated/path |  | ||||||
|           to: /new/path |  | ||||||
|         - type: proxy |  | ||||||
|           port: 3000 |  | ||||||
|       dns: |  | ||||||
|         - type: 'dns@oauth3.org' |  | ||||||
|           token_id: user_token_id |  | ||||||
| 
 |  | ||||||
|   - names: |  | ||||||
|       - ssh.example.com |  | ||||||
|     modules: |  | ||||||
|       tls: |  | ||||||
|         - type: acme |  | ||||||
|           email: john.smith@example.com |  | ||||||
|           challenge_type: 'http-01' |  | ||||||
|       tcp: |  | ||||||
|         - type: proxy |  | ||||||
|           port: 22 |  | ||||||
|       dns: |  | ||||||
|         - type: 'dns@oauth3.org' |  | ||||||
|           token_id: user_token_id |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ### tunnel\_server |  | ||||||
| 
 |  | ||||||
| The tunnel server system is meant to be run on a publicly accessible IP address to server tunnel clients |  | ||||||
| which are behind firewalls, carrier-grade NAT, or otherwise Internet-connect but inaccessible devices. |  | ||||||
| 
 |  | ||||||
| It has the following options: |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| secret          A 128-bit or greater string to use for signing tokens (HMAC JWT) |  | ||||||
|                 ex: abc123 |  | ||||||
| 
 |  | ||||||
| servernames     An array of string servernames that should be captured as the |  | ||||||
|                 tunnel server, ignoring the TLS forward module |  | ||||||
|                 ex: api.tunnel.example.com |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Example config: |  | ||||||
| 
 |  | ||||||
| ```yml |  | ||||||
| tunnel_server: |  | ||||||
|   secret: abc123def456ghi789 |  | ||||||
|   servernames: |  | ||||||
|     - 'api.tunnel.example.com' |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### DDNS |  | ||||||
| 
 |  | ||||||
| The DDNS module watches the network environment of the unit and makes sure the |  | ||||||
| device is always accessible on the internet using the domains listed in the |  | ||||||
| config. If the device has a public address or if it can automatically set up |  | ||||||
| port forwarding the device will periodically check its public address to ensure |  | ||||||
| the DNS records always point to it. Otherwise it will to connect to a tunnel |  | ||||||
| server and set the DNS records to point to that server. |  | ||||||
| 
 |  | ||||||
| The `loopback` setting specifies how the unit will check its public IP address |  | ||||||
| and whether connections can reach it. Currently only `tunnel@oauth3.org` is |  | ||||||
| supported. If the loopback setting is not defined it will default to using |  | ||||||
| `oauth3.org`. |  | ||||||
| 
 |  | ||||||
| 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. |  | ||||||
| 
 |  | ||||||
| If a particular DDNS module has been disabled the device will still try to set |  | ||||||
| up port forwarding (and connect to a tunnel if that doesn't work), but the DNS |  | ||||||
| records will not be updated to point to the device. This is to allow a setup to |  | ||||||
| be tested before transitioning services between devices. |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| ddns: |  | ||||||
|   disabled: false |  | ||||||
|   loopback: |  | ||||||
|     type: 'tunnel@oauth3.org' |  | ||||||
|     domain: oauth3.org |  | ||||||
|   tunnel: |  | ||||||
|     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 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### mDNS |  | ||||||
| 
 |  | ||||||
| enabled by default |  | ||||||
| 
 |  | ||||||
| Although it does not announce itself, Goldilocks is discoverable via mDNS with the special query `_cloud._tcp.local`. |  | ||||||
| This is so that it can be easily configured via Desktop and Mobile apps when run on devices such as a Raspberry Pi or |  | ||||||
| SOHO servers. |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| mdns: |  | ||||||
|   disabled: false |  | ||||||
|   port: 5353 |  | ||||||
|   broadcast: '224.0.0.251' |  | ||||||
|   ttl: 300 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| You can discover goldilocks with `mdig`. |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| npm install -g git+https://git.coolaj86.com/coolaj86/mdig.js.git |  | ||||||
| 
 |  | ||||||
| mdig _cloud._tcp.local |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### socks5 |  | ||||||
| 
 |  | ||||||
| Run a Socks5 proxy server. |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| socks5: |  | ||||||
|   enable: true |  | ||||||
|   port: 1080 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### api |  | ||||||
| 
 |  | ||||||
| See [API.md](/API.md) |  | ||||||
| 
 |  | ||||||
| @tigerbot: How are the APIs used (in terms of URL, Method, Headers, etc)? |  | ||||||
| 
 |  | ||||||
| TODO |  | ||||||
| ---- |  | ||||||
| 
 |  | ||||||
| * [ ] http - nowww module |  | ||||||
| * [ ] http - Allow match styles of `www.*`, `*`, and `*.example.com` equally |  | ||||||
| * [ ] http - redirect based on domain name (not just path) |  | ||||||
| * [ ] tcp - bind should be able to specify localhost, uniquelocal, private, or ip |  | ||||||
| * [ ] tcp - if destination host is omitted default to localhost, if dst port is missing, default to src |  | ||||||
| * [ ] sys - `curl https://coolaj86.com/goldilocks | bash -s example.com` |  | ||||||
| * [ ] oauth3 - `example.com/.well-known/domains@oauth3.org/directives.json` |  | ||||||
| * [ ] oauth3 - commandline questionnaire |  | ||||||
| * [x] modules - use consistent conventions (i.e. address vs host + port) |  | ||||||
|   * [x] tls - tls.acme vs tls.modules.acme |  | ||||||
| * [ ] tls - forward should be able to match on source port to reach different destination ports |  | ||||||
|  | |||||||
| @ -11,12 +11,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|       return Oauth3.PromiseA.resolve(session); |       return Oauth3.PromiseA.resolve(session); | ||||||
|     }; |     }; | ||||||
|     var auth = Oauth3.create(); |     var auth = Oauth3.create(); | ||||||
|     auth.setProvider('oauth3.org').then(function () { |     auth.setProvider('oauth3.org'); | ||||||
|       auth.checkSession().then(function (session) { |  | ||||||
|         console.log('hasSession?', session); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     window.oauth3 = auth; // debug
 |     window.oauth3 = auth; // debug
 | ||||||
|     return auth; |     return auth; | ||||||
|   } ]) |   } ]) | ||||||
| @ -144,13 +139,8 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
| 
 | 
 | ||||||
|     vm.authenticate = function () { |     vm.authenticate = function () { | ||||||
|       // TODO authorization redirect /api/org.oauth3.consumer/authorization_redirect/:provider_uri
 |       // TODO authorization redirect /api/org.oauth3.consumer/authorization_redirect/:provider_uri
 | ||||||
|       var opts = { |  | ||||||
|         type: 'popup' |  | ||||||
|       , scope: 'domains,dns' |  | ||||||
|       // , debug: true
 |  | ||||||
|       }; |  | ||||||
| 
 | 
 | ||||||
|       return oauth3.authenticate(opts).then(function (session) { |       return oauth3.authenticate().then(function (session) { | ||||||
|         console.info("Authorized Session", session); |         console.info("Authorized Session", session); | ||||||
| 
 | 
 | ||||||
|         return oauth3.api('domains.list').then(function (domains) { |         return oauth3.api('domains.list').then(function (domains) { | ||||||
| @ -161,7 +151,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
| 
 | 
 | ||||||
|             return OAUTH3.request({ |             return OAUTH3.request({ | ||||||
|               method: 'POST' |               method: 'POST' | ||||||
|             , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/init' |             , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/init' | ||||||
|             , session: session |             , session: session | ||||||
|             , data: { |             , data: { | ||||||
|                 access_token: session.access_token |                 access_token: session.access_token | ||||||
| @ -185,7 +175,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|               console.info('Initialized Goldilocks', resp); |               console.info('Initialized Goldilocks', resp); | ||||||
|               return OAUTH3.request({ |               return OAUTH3.request({ | ||||||
|                 method: 'GET' |                 method: 'GET' | ||||||
|               , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/config' |               , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/config' | ||||||
|               , session: session |               , session: session | ||||||
|               }).then(function (configResp) { |               }).then(function (configResp) { | ||||||
|                 console.log('config', configResp.data); |                 console.log('config', configResp.data); | ||||||
| @ -223,7 +213,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|                 vm.admin.network.iface = 'gateway'; |                 vm.admin.network.iface = 'gateway'; | ||||||
|                 return OAUTH3.request({ |                 return OAUTH3.request({ | ||||||
|                   method: 'POST' |                   method: 'POST' | ||||||
|                 , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/request' |                 , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/request' | ||||||
|                 , session: session |                 , session: session | ||||||
|                 , data: { |                 , data: { | ||||||
|                     method: 'GET' |                     method: 'GET' | ||||||
| @ -250,15 +240,24 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     vm.enableTunnel = function (/*opts*/) { |     vm.enableTunnel = function (/*opts*/) { | ||||||
|  |       vm.admin.network.iface = 'oauth3-tunnel'; | ||||||
|  | 
 | ||||||
|       return oauth3.request({ |       return oauth3.request({ | ||||||
|         method: 'POST' |         method: 'POST' | ||||||
|       , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/tunnel' |       , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/tunnel' | ||||||
|       }).then(function (result) { |       /* | ||||||
|         // vm.admin.network.iface = 'oauth3-tunnel';
 |       , data: { | ||||||
|         return result; |           method: 'GET' | ||||||
|  |         , url: 'https://api.ipify.org?format=json' | ||||||
|  |         } | ||||||
|  |       */ | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     oauth3.checkSession().then(function (session) { | ||||||
|  |       console.log('hasSession?', session); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     /* |     /* | ||||||
|     console.log('OAUTH3.PromiseA', OAUTH3.PromiseA); |     console.log('OAUTH3.PromiseA', OAUTH3.PromiseA); | ||||||
|     return oauth3.setProvider('oauth3.org').then(function () { |     return oauth3.setProvider('oauth3.org').then(function () { | ||||||
|  | |||||||
							
								
								
									
										1091
									
								
								bin/goldilocks.js
									
									
									
									
									
								
							
							
						
						
									
										1091
									
								
								bin/goldilocks.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,57 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8"?> |  | ||||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |  | ||||||
| <plist version="1.0"> |  | ||||||
| <dict> |  | ||||||
| 	<key>Label</key> |  | ||||||
| 	<string>Goldilocks</string> |  | ||||||
| 	<key>ProgramArguments</key> |  | ||||||
| 	<array> |  | ||||||
| 		<string>/opt/goldilocks/bin/node</string> |  | ||||||
| 		<string>/opt/goldilocks/bin/goldilocks</string> |  | ||||||
| 		<string>--config</string> |  | ||||||
| 		<string>/etc/goldilocks/goldilocks.yml</string> |  | ||||||
| 	</array> |  | ||||||
| 	<key>EnvironmentVariables</key> |  | ||||||
| 	<dict> |  | ||||||
| 		<key>GOLDILOCKS_PATH</key> |  | ||||||
| 		<string>/opt/goldilocks</string> |  | ||||||
| 		<key>NODE_PATH</key> |  | ||||||
| 		<string>/opt/goldilocks/lib/node_modules</string> |  | ||||||
| 		<key>NPM_CONFIG_PREFIX</key> |  | ||||||
| 		<string>/opt/goldilocks</string> |  | ||||||
| 	</dict> |  | ||||||
| 
 |  | ||||||
| 	<key>UserName</key> |  | ||||||
| 	<string>root</string> |  | ||||||
| 	<key>GroupName</key> |  | ||||||
| 	<string>wheel</string> |  | ||||||
| 	<key>InitGroups</key> |  | ||||||
| 	<true/> |  | ||||||
| 
 |  | ||||||
| 	<key>RunAtLoad</key> |  | ||||||
| 	<true/> |  | ||||||
| 	<key>KeepAlive</key> |  | ||||||
| 	<dict> |  | ||||||
| 		<key>Crashed</key> |  | ||||||
| 		<true/> |  | ||||||
| 		<key>SuccessfulExit</key> |  | ||||||
| 		<false/> |  | ||||||
| 	</dict> |  | ||||||
| 
 |  | ||||||
| 	<key>SoftResourceLimits</key> |  | ||||||
| 	<dict> |  | ||||||
| 		<key>NumberOfFiles</key> |  | ||||||
| 		<integer>8192</integer> |  | ||||||
| 	</dict> |  | ||||||
| 	<key>HardResourceLimits</key> |  | ||||||
| 	<dict/> |  | ||||||
| 
 |  | ||||||
| 	<key>WorkingDirectory</key> |  | ||||||
|   <string>/srv/www</string> |  | ||||||
| 
 |  | ||||||
| 	<key>StandardErrorPath</key> |  | ||||||
| 	<string>/var/log/goldilocks/error.log</string> |  | ||||||
| 	<key>StandardOutPath</key> |  | ||||||
| 	<string>/var/log/goldilocks/info.log</string> |  | ||||||
| </dict> |  | ||||||
| </plist> |  | ||||||
							
								
								
									
										106
									
								
								dist/etc/goldilocks/goldilocks.example.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										106
									
								
								dist/etc/goldilocks/goldilocks.example.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,106 +0,0 @@ | |||||||
| tcp: |  | ||||||
|   bind: |  | ||||||
|     - 22 |  | ||||||
|     - 80 |  | ||||||
|     - 443 |  | ||||||
|   modules: |  | ||||||
|     - type: forward |  | ||||||
|       ports: |  | ||||||
|         - 22 |  | ||||||
|       address: '127.0.0.1:8022' |  | ||||||
| 
 |  | ||||||
| udp: |  | ||||||
|   bind: |  | ||||||
|     - 53 |  | ||||||
|   modules: |  | ||||||
|     - type: forward |  | ||||||
|       ports: |  | ||||||
|         - 53 |  | ||||||
|       port: 5353 |  | ||||||
|       # default host is localhost |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| tls: |  | ||||||
|   modules: |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - localhost.bar.daplie.me |  | ||||||
|         - localhost.foo.daplie.me |  | ||||||
|       address: '127.0.0.1:5443' |  | ||||||
|     - type: acme |  | ||||||
|       domains: |  | ||||||
|         - '*.localhost.daplie.me' |  | ||||||
|       email: 'guest@example.com' |  | ||||||
|       challenge_type: 'http-01' |  | ||||||
| 
 |  | ||||||
| http: |  | ||||||
|   trust_proxy: true |  | ||||||
|   allow_insecure: false |  | ||||||
|   primary_domain: localhost.daplie.me |  | ||||||
| 
 |  | ||||||
|   modules: |  | ||||||
|     - type: redirect |  | ||||||
|       domains: |  | ||||||
|         - localhost.beta.daplie.me |  | ||||||
|       status: 301 |  | ||||||
|       from: /old/path/*/other/* |  | ||||||
|       to: https://example.com/path/new/:2/something/:1 |  | ||||||
|     - type: proxy |  | ||||||
|       domains: |  | ||||||
|         - localhost.daplie.me |  | ||||||
|       host: localhost |  | ||||||
|       port: 4000 |  | ||||||
|     - type: static |  | ||||||
|       domains: |  | ||||||
|         - '*.localhost.daplie.me' |  | ||||||
|       root: '/srv/www/:hostname' |  | ||||||
| 
 |  | ||||||
| domains: |  | ||||||
|   - names: |  | ||||||
|       - localhost.gamma.daplie.me |  | ||||||
|     modules: |  | ||||||
|       tls: |  | ||||||
|         - type: proxy |  | ||||||
|           port: 6443 |  | ||||||
|   - names: |  | ||||||
|       - beta.localhost.daplie.me |  | ||||||
|       - baz.localhost.daplie.me |  | ||||||
|     modules: |  | ||||||
|       tls: |  | ||||||
|         - type: acme |  | ||||||
|           email: 'owner@example.com' |  | ||||||
|           challenge_type: 'tls-sni-01' |  | ||||||
|           # default server is 'https://acme-v01.api.letsencrypt.org/directory' |  | ||||||
|       http: |  | ||||||
|         - type: redirect |  | ||||||
|           from: /nowhere/in/particular |  | ||||||
|           to: /just/an/example |  | ||||||
|         - type: proxy |  | ||||||
|           address: '127.0.0.1:3001' |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| mdns: |  | ||||||
|   disabled: false |  | ||||||
|   port: 5353 |  | ||||||
|   broadcast: '224.0.0.251' |  | ||||||
|   ttl: 300 |  | ||||||
| 
 |  | ||||||
| tunnel_server: |  | ||||||
|   secret: abc123 |  | ||||||
|   servernames: |  | ||||||
|     - 'tunnel.localhost.com' |  | ||||||
| 
 |  | ||||||
| ddns: |  | ||||||
|   loopback: |  | ||||||
|     type: 'tunnel@oauth3.org' |  | ||||||
|     domain: oauth3.org |  | ||||||
|   tunnel: |  | ||||||
|     type: 'tunnel@oauth3.org' |  | ||||||
|     token: user_token_id |  | ||||||
|   modules: |  | ||||||
|     - type: 'dns@oauth3.org' |  | ||||||
|       token: user_token_id |  | ||||||
|       domains: |  | ||||||
|         - www.example.com |  | ||||||
|         - api.example.com |  | ||||||
|         - test.example.com |  | ||||||
							
								
								
									
										0
									
								
								dist/etc/goldilocks/goldilocks.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										0
									
								
								dist/etc/goldilocks/goldilocks.yml
									
									
									
									
										vendored
									
									
								
							
							
								
								
									
										69
									
								
								dist/etc/systemd/system/goldilocks.service
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										69
									
								
								dist/etc/systemd/system/goldilocks.service
									
									
									
									
										vendored
									
									
								
							| @ -1,69 +0,0 @@ | |||||||
| [Unit] |  | ||||||
| Description=Goldilocks Internet Server |  | ||||||
| Documentation=https://git.daplie.com/Daplie/goldilocks.js |  | ||||||
| After=network-online.target |  | ||||||
| Wants=network-online.target systemd-networkd-wait-online.service |  | ||||||
| 
 |  | ||||||
| [Service] |  | ||||||
| # Restart on crash (bad signal), and on 'clean' failure (error exit code) |  | ||||||
| # Allow up to 3 restarts within 10 seconds |  | ||||||
| # (it's unlikely that a user or properly-running script will do this) |  | ||||||
| Restart=on-failure |  | ||||||
| StartLimitInterval=10 |  | ||||||
| StartLimitBurst=3 |  | ||||||
| 
 |  | ||||||
| # The v8 VM will output a "clean" for JavaScript errors. |  | ||||||
| # If we knew we were never going to accidentally exit cleanly |  | ||||||
| # we would use on-abnormal: |  | ||||||
| ; Restart=on-abnormal |  | ||||||
| 
 |  | ||||||
| # User and group the process will run as |  | ||||||
| # (www-data is the de facto standard on most systems) |  | ||||||
| User=MY_USER |  | ||||||
| Group=MY_GROUP |  | ||||||
| 
 |  | ||||||
| # If we need to pass environment variables in the future |  | ||||||
| Environment=GOLDILOCKS_PATH=/srv/www NODE_PATH=/opt/goldilocks/lib/node_modules NPM_CONFIG_PREFIX=/opt/goldilocks |  | ||||||
| 
 |  | ||||||
| # Set a sane working directory, sane flags, and specify how to reload the config file |  | ||||||
| WorkingDirectory=/opt/goldilocks |  | ||||||
| ExecStart=/opt/goldilocks/bin/node /opt/goldilocks/bin/goldilocks --config /etc/goldilocks/goldilocks.yml |  | ||||||
| ExecReload=/bin/kill -USR1 $MAINPID |  | ||||||
| 
 |  | ||||||
| # Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings. |  | ||||||
| # Unmodified goldilocks is not expected to use more than this. |  | ||||||
| LimitNOFILE=1048576 |  | ||||||
| LimitNPROC=64 |  | ||||||
| 
 |  | ||||||
| # Use private /tmp and /var/tmp, which are discarded after goldilocks stops. |  | ||||||
| PrivateTmp=true |  | ||||||
| # Use a minimal /dev |  | ||||||
| PrivateDevices=true |  | ||||||
| # Hide /home, /root, and /run/user. Nobody will steal your SSH-keys. |  | ||||||
| ProtectHome=true |  | ||||||
| # Make /usr, /boot, /etc and possibly some more folders read-only. |  | ||||||
| ProtectSystem=full |  | ||||||
| # … except TLS/SSL, ACME, and Let's Encrypt certificates |  | ||||||
| #   and /var/log/goldilocks, because we want a place where logs can go. |  | ||||||
| #   This merely retains r/w access rights, it does not add any new. Must still be writable on the host! |  | ||||||
| ReadWriteDirectories=/etc/goldilocks /etc/ssl /srv/www /var/log/goldilocks /opt/goldilocks |  | ||||||
| # you may also want to add other directories such as /opt/goldilocks /etc/acme /etc/letsencrypt |  | ||||||
| 
 |  | ||||||
| # Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories |  | ||||||
| ; ReadWritePaths=/etc/goldilocks /var/log/goldilocks |  | ||||||
| 
 |  | ||||||
| # The following additional security directives only work with systemd v229 or later. |  | ||||||
| # They further retrict privileges that can be gained. |  | ||||||
| # Note that you may have to add capabilities required by any plugins in use. |  | ||||||
| CapabilityBoundingSet=CAP_NET_BIND_SERVICE |  | ||||||
| AmbientCapabilities=CAP_NET_BIND_SERVICE |  | ||||||
| NoNewPrivileges=true |  | ||||||
| 
 |  | ||||||
| # Caveat: Some plugins need additional capabilities. |  | ||||||
| # For example "upload" needs CAP_LEASE |  | ||||||
| ; CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_LEASE |  | ||||||
| ; AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_LEASE |  | ||||||
| ; NoNewPrivileges=true |  | ||||||
| 
 |  | ||||||
| [Install] |  | ||||||
| WantedBy=multi-user.target |  | ||||||
							
								
								
									
										5
									
								
								dist/etc/tmpfiles.d/goldilocks.conf
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								dist/etc/tmpfiles.d/goldilocks.conf
									
									
									
									
										vendored
									
									
								
							| @ -1,5 +0,0 @@ | |||||||
| # /etc/tmpfiles.d/goldilocks.conf |  | ||||||
| # See https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html |  | ||||||
| 
 |  | ||||||
| # Type Path           Mode UID      GID      Age Argument |  | ||||||
| d /run/goldilocks     0755 MY_USER  MY_GROUP -   - |  | ||||||
| @ -1,20 +0,0 @@ | |||||||
| set -e |  | ||||||
| set -u |  | ||||||
| 
 |  | ||||||
| my_name=goldilocks |  | ||||||
| # TODO provide an option to supply my_ver and my_tmp |  | ||||||
| my_ver=master |  | ||||||
| my_tmp=$(mktemp -d) |  | ||||||
| 
 |  | ||||||
| mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name |  | ||||||
| git clone https://git.coolaj86.com/coolaj86/goldilocks.js.git $my_tmp/opt/$my_name/lib/node_modules/$my_name |  | ||||||
| 
 |  | ||||||
| echo "Installing to $my_tmp (will be moved after install)" |  | ||||||
| pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name |  | ||||||
|   git checkout $my_ver |  | ||||||
|   source ./installer/install.sh |  | ||||||
| popd |  | ||||||
| 
 |  | ||||||
| echo "Installation successful, now cleaning up $my_tmp ..." |  | ||||||
| rm -rf $my_tmp |  | ||||||
| echo "Done" |  | ||||||
| @ -1,48 +0,0 @@ | |||||||
| ############################### |  | ||||||
| #                             # |  | ||||||
| #         http_get            # |  | ||||||
| # boilerplate for curl / wget # |  | ||||||
| #                             # |  | ||||||
| ############################### |  | ||||||
| 
 |  | ||||||
| # See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh |  | ||||||
| 
 |  | ||||||
| _h_http_get="" |  | ||||||
| _h_http_opts="" |  | ||||||
| _h_http_out="" |  | ||||||
| 
 |  | ||||||
| detect_http_get() |  | ||||||
| { |  | ||||||
|   set +e |  | ||||||
|   if type -p curl >/dev/null 2>&1; then |  | ||||||
|     _h_http_get="curl" |  | ||||||
|     _h_http_opts="-fsSL" |  | ||||||
|     _h_http_out="-o" |  | ||||||
|   elif type -p wget >/dev/null 2>&1; then |  | ||||||
|     _h_http_get="wget" |  | ||||||
|     _h_http_opts="--quiet" |  | ||||||
|     _h_http_out="-O" |  | ||||||
|   else |  | ||||||
|     echo "Aborted, could not find curl or wget" |  | ||||||
|     return 7 |  | ||||||
|   fi |  | ||||||
|   set -e |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| http_get() |  | ||||||
| { |  | ||||||
|   $_h_http_get $_h_http_opts $_h_http_out "$2" "$1" |  | ||||||
|   touch "$2" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| http_bash() |  | ||||||
| { |  | ||||||
|   _http_url=$1 |  | ||||||
|   #dap_args=$2 |  | ||||||
|   rm -rf dap-tmp-runner.sh |  | ||||||
|   $_h_http_get $_h_http_opts $_h_http_out dap-tmp-runner.sh "$_http_url"; bash dap-tmp-runner.sh; rm dap-tmp-runner.sh |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| detect_http_get |  | ||||||
| 
 |  | ||||||
| ## END HTTP_GET ## |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| set -u |  | ||||||
| 
 |  | ||||||
| my_app_launchd_service="Library/LaunchDaemons/${my_app_pkg_name}.plist" |  | ||||||
| 
 |  | ||||||
| echo "" |  | ||||||
| echo "Installing as launchd service" |  | ||||||
| echo "" |  | ||||||
| 
 |  | ||||||
| # See http://www.launchd.info/ |  | ||||||
| safe_copy_config "$my_app_dist/$my_app_launchd_service" "$my_root/$my_app_launchd_service" |  | ||||||
| 
 |  | ||||||
| $sudo_cmd chown root:wheel "$my_root/$my_app_launchd_service" |  | ||||||
| 
 |  | ||||||
| $sudo_cmd launchctl unload -w "$my_root/$my_app_launchd_service" >/dev/null 2>/dev/null |  | ||||||
| $sudo_cmd launchctl load -w "$my_root/$my_app_launchd_service" |  | ||||||
| 
 |  | ||||||
| echo "$my_app_name started with launchd" |  | ||||||
| @ -1,37 +0,0 @@ | |||||||
| set -u |  | ||||||
| 
 |  | ||||||
| my_app_systemd_service="etc/systemd/system/${my_app_name}.service" |  | ||||||
| my_app_systemd_tmpfiles="etc/tmpfiles.d/${my_app_name}.conf" |  | ||||||
| 
 |  | ||||||
| echo "" |  | ||||||
| echo "Installing as systemd service" |  | ||||||
| echo "" |  | ||||||
| 
 |  | ||||||
| sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_service" > "$my_app_dist/$my_app_systemd_service.2" |  | ||||||
| sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_service.2" > "$my_app_dist/$my_app_systemd_service" |  | ||||||
| rm "$my_app_dist/$my_app_systemd_service.2" |  | ||||||
| safe_copy_config "$my_app_dist/$my_app_systemd_service" "$my_root/$my_app_systemd_service" |  | ||||||
| $sudo_cmd chown root:root "$my_root/$my_app_systemd_service" |  | ||||||
| 
 |  | ||||||
| sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_tmpfiles" > "$my_app_dist/$my_app_systemd_tmpfiles.2" |  | ||||||
| sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_tmpfiles.2" > "$my_app_dist/$my_app_systemd_tmpfiles" |  | ||||||
| rm "$my_app_dist/$my_app_systemd_tmpfiles.2" |  | ||||||
| safe_copy_config "$my_app_dist/$my_app_systemd_tmpfiles" "$my_root/$my_app_systemd_tmpfiles" |  | ||||||
| $sudo_cmd chown root:root "$my_root/$my_app_systemd_tmpfiles" |  | ||||||
| 
 |  | ||||||
| $sudo_cmd systemctl stop "${my_app_name}.service" >/dev/null 2>/dev/null || true |  | ||||||
| $sudo_cmd systemctl daemon-reload |  | ||||||
| $sudo_cmd systemctl start "${my_app_name}.service" |  | ||||||
| $sudo_cmd systemctl enable "${my_app_name}.service" |  | ||||||
| 
 |  | ||||||
| echo "" |  | ||||||
| echo "" |  | ||||||
| echo "Fun systemd commands to remember:" |  | ||||||
| echo "  $sudo_cmd systemctl daemon-reload" |  | ||||||
| echo "  $sudo_cmd systemctl restart $my_app_name.service" |  | ||||||
| echo "" |  | ||||||
| echo "$my_app_name started with systemctl, check its status like so:" |  | ||||||
| echo "  $sudo_cmd systemctl status $my_app_name" |  | ||||||
| echo "  $sudo_cmd journalctl -xefu $my_app_name" |  | ||||||
| echo "" |  | ||||||
| echo "" |  | ||||||
| @ -1,37 +0,0 @@ | |||||||
| safe_copy_config() |  | ||||||
| { |  | ||||||
|   src=$1 |  | ||||||
|   dst=$2 |  | ||||||
|   $sudo_cmd mkdir -p $(dirname "$dst") |  | ||||||
|   if [ -f "$dst" ]; then |  | ||||||
|     $sudo_cmd rsync -a "$src" "$dst.latest" |  | ||||||
|     # TODO edit config file with $my_user and $my_group |  | ||||||
|     if [ "$(cat $dst)" == "$(cat $dst.latest)" ]; then |  | ||||||
|       $sudo_cmd rm $dst.latest |  | ||||||
|     else |  | ||||||
|       echo "MANUAL INTERVENTION REQUIRED: check the systemd script update and manually decide what you want to do" |  | ||||||
|       echo "diff $dst $dst.latest" |  | ||||||
|       $sudo_cmd chown -R root:root "$dst.latest" |  | ||||||
|     fi |  | ||||||
|   else |  | ||||||
|     $sudo_cmd rsync -a --ignore-existing "$src" "$dst" |  | ||||||
|   fi |  | ||||||
|   $sudo_cmd chown -R root:root "$dst" |  | ||||||
|   $sudo_cmd chmod 644 "$dst" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| installable="" |  | ||||||
| if [ -d "$my_root/etc/systemd/system" ]; then |  | ||||||
|   source ./installer/install-for-systemd.sh |  | ||||||
|   installable="true" |  | ||||||
| fi |  | ||||||
| if [ -d "/Library/LaunchDaemons" ]; then |  | ||||||
|   source ./installer/install-for-launchd.sh |  | ||||||
|   installable="true" |  | ||||||
| fi |  | ||||||
| if [ -z "$installable" ]; then |  | ||||||
|   echo "" |  | ||||||
|   echo "Unknown system service init type. You must install as a system service manually." |  | ||||||
|   echo '(please file a bug with the output of "uname -a")' |  | ||||||
|   echo "" |  | ||||||
| fi |  | ||||||
| @ -1,150 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
| 
 |  | ||||||
| set -e |  | ||||||
| set -u |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ### IMPORTANT ### |  | ||||||
| ###  VERSION  ### |  | ||||||
| my_name=goldilocks |  | ||||||
| my_app_pkg_name=com.coolaj86.goldilocks.web |  | ||||||
| my_app_ver="v1.1" |  | ||||||
| my_azp_oauth3_ver="v1.2.3" |  | ||||||
| export NODE_VERSION="v8.9.3" |  | ||||||
| 
 |  | ||||||
| if [ -z "${my_tmp-}" ]; then |  | ||||||
|   my_tmp="$(mktemp -d)" |  | ||||||
|   mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name |  | ||||||
|   echo "Installing to $my_tmp (will be moved after install)" |  | ||||||
|   git clone ./ $my_tmp/opt/$my_name/lib/node_modules/$my_name |  | ||||||
|   pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name |  | ||||||
| fi |  | ||||||
| 
 |  | ||||||
| ################# |  | ||||||
| export NODE_PATH=$my_tmp/opt/$my_name/lib/node_modules |  | ||||||
| export PATH=$my_tmp/opt/$my_name/bin/:$PATH |  | ||||||
| export NPM_CONFIG_PREFIX=$my_tmp/opt/$my_name |  | ||||||
| my_npm="$NPM_CONFIG_PREFIX/bin/npm" |  | ||||||
| ################# |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| my_app_dist=$my_tmp/opt/$my_name/lib/node_modules/$my_name/dist |  | ||||||
| installer_base="https://git.coolaj86.com/coolaj86/goldilocks.js/raw/$my_app_ver" |  | ||||||
| 
 |  | ||||||
| # Backwards compat |  | ||||||
| # some scripts still use the old names |  | ||||||
| my_app_dir=$my_tmp |  | ||||||
| my_app_name=$my_name |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| git checkout $my_app_ver |  | ||||||
| 
 |  | ||||||
| mkdir -p "$my_tmp/opt/$my_name"/{lib,bin,etc} |  | ||||||
| ln -s ../lib/node_modules/$my_name/bin/$my_name.js $my_tmp/opt/$my_name/bin/$my_name |  | ||||||
| ln -s ../lib/node_modules/$my_name/bin/$my_name.js $my_tmp/opt/$my_name/bin/$my_name.js |  | ||||||
| mkdir -p "$my_tmp/etc/$my_name" |  | ||||||
| chmod 775 "$my_tmp/etc/$my_name" |  | ||||||
| cat "$my_app_dist/etc/$my_name/$my_name.example.yml" > "$my_tmp/etc/$my_name/$my_name.example.yml" |  | ||||||
| chmod 664 "$my_tmp/etc/$my_name/$my_name.example.yml" |  | ||||||
| mkdir -p $my_tmp/srv/www |  | ||||||
| mkdir -p $my_tmp/var/www |  | ||||||
| mkdir -p $my_tmp/var/log/$my_name |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # |  | ||||||
| # Helpers |  | ||||||
| # |  | ||||||
| source ./installer/sudo-cmd.sh |  | ||||||
| source ./installer/http-get.sh |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # |  | ||||||
| # Dependencies |  | ||||||
| # |  | ||||||
| echo $NODE_VERSION > /tmp/NODEJS_VER |  | ||||||
| http_bash "https://git.coolaj86.com/coolaj86/node-installer.sh/raw/v1.1/install.sh" |  | ||||||
| $my_npm install -g npm@4 |  | ||||||
| pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name |  | ||||||
|   $my_npm install |  | ||||||
| popd |  | ||||||
| pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name/packages/assets |  | ||||||
|   OAUTH3_GIT_URL="https://git.oauth3.org/OAuth3/oauth3.js.git" |  | ||||||
|   git clone ${OAUTH3_GIT_URL} oauth3.org || true |  | ||||||
|   ln -s oauth3.org org.oauth3 |  | ||||||
|   pushd oauth3.org |  | ||||||
|     git remote set-url origin ${OAUTH3_GIT_URL} |  | ||||||
|     git checkout $my_azp_oauth3_ver |  | ||||||
|     #git pull |  | ||||||
|   popd |  | ||||||
| 
 |  | ||||||
|   mkdir -p jquery.com |  | ||||||
|   ln -s jquery.com com.jquery |  | ||||||
|   pushd jquery.com |  | ||||||
|     http_get 'https://code.jquery.com/jquery-3.1.1.js' jquery-3.1.1.js |  | ||||||
|   popd |  | ||||||
| 
 |  | ||||||
|   mkdir -p google.com |  | ||||||
|   ln -s google.com com.google |  | ||||||
|   pushd google.com |  | ||||||
|     http_get 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js' angular.1.6.2.min.js |  | ||||||
|   popd |  | ||||||
| 
 |  | ||||||
|   mkdir -p well-known |  | ||||||
|   ln -s well-known .well-known |  | ||||||
|   pushd well-known |  | ||||||
|     ln -snf ../oauth3.org/well-known/oauth3 ./oauth3 |  | ||||||
|   popd |  | ||||||
|   echo "installed dependencies" |  | ||||||
| popd |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # |  | ||||||
| # System Service |  | ||||||
| # |  | ||||||
| source ./installer/my-root.sh |  | ||||||
| echo "Pre-installation to $my_tmp complete, now installing to $my_root/ ..." |  | ||||||
| set +e |  | ||||||
| if type -p tree >/dev/null 2>/dev/null; then |  | ||||||
|   #tree -I "node_modules|include|share" $my_tmp |  | ||||||
|   tree -L 6 -I "include|share|npm" $my_tmp |  | ||||||
| else |  | ||||||
|   ls $my_tmp |  | ||||||
| fi |  | ||||||
| set -e |  | ||||||
| 
 |  | ||||||
| source ./installer/my-user-my-group.sh |  | ||||||
| echo "User $my_user Group $my_group" |  | ||||||
| 
 |  | ||||||
| source ./installer/install-system-service.sh |  | ||||||
| 
 |  | ||||||
| $sudo_cmd chown -R $my_user:$my_group $my_tmp/* |  | ||||||
| $sudo_cmd chown root:root $my_tmp/* |  | ||||||
| $sudo_cmd chown root:root $my_tmp |  | ||||||
| $sudo_cmd chmod 0755 $my_tmp |  | ||||||
| # don't change permissions on /, /etc, etc |  | ||||||
| $sudo_cmd rsync -a --ignore-existing $my_tmp/ $my_root/ |  | ||||||
| $sudo_cmd rsync -a --ignore-existing $my_app_dist/etc/$my_name/$my_name.yml $my_root/etc/$my_name/$my_name.yml |  | ||||||
| 
 |  | ||||||
| # Change to admin perms |  | ||||||
| $sudo_cmd chown -R $my_user:$my_group $my_root/opt/$my_name |  | ||||||
| $sudo_cmd chown -R $my_user:$my_group $my_root/var/www $my_root/srv/www |  | ||||||
| 
 |  | ||||||
| # make sure the files are all read/write for the owner and group, and then set |  | ||||||
| # the setuid and setgid bits so that any files/directories created inside these |  | ||||||
| # directories have the same owner and group. |  | ||||||
| $sudo_cmd chmod -R ug+rwX $my_root/opt/$my_name |  | ||||||
| find $my_root/opt/$my_name -type d -exec $sudo_cmd chmod ug+s {} \; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| echo "" |  | ||||||
| echo "$my_name installation complete!" |  | ||||||
| echo "" |  | ||||||
| echo "" |  | ||||||
| echo "Update the config at: /etc/$my_name/$my_name.yml" |  | ||||||
| echo "" |  | ||||||
| echo "Unistall: rm -rf /srv/$my_name/ /var/$my_name/ /etc/$my_name/ /opt/$my_name/ /var/log/$my_name/ /etc/tmpfiles.d/$my_name.conf /etc/systemd/system/$my_name.service /etc/ssl/$my_name" |  | ||||||
| @ -1,8 +0,0 @@ | |||||||
| # something or other about android and tmux using PREFIX |  | ||||||
| #: "${PREFIX:=''}" |  | ||||||
| my_root="" |  | ||||||
| if [ -z "${PREFIX-}" ]; then |  | ||||||
|   my_root="" |  | ||||||
| else |  | ||||||
|   my_root="$PREFIX" |  | ||||||
| fi |  | ||||||
| @ -1,19 +0,0 @@ | |||||||
| if type -p adduser >/dev/null 2>/dev/null; then |  | ||||||
|   if [ -z "$(cat $my_root/etc/passwd | grep $my_app_name)" ]; then |  | ||||||
|     $sudo_cmd adduser --home $my_root/opt/$my_app_name --gecos '' --disabled-password $my_app_name |  | ||||||
|   fi |  | ||||||
|   my_user=$my_app_name |  | ||||||
|   my_group=$my_app_name |  | ||||||
| elif [ -n "$(cat /etc/passwd | grep www-data:)" ]; then |  | ||||||
|   # Linux (Ubuntu) |  | ||||||
|   my_user=www-data |  | ||||||
|   my_group=www-data |  | ||||||
| elif [ -n "$(cat /etc/passwd | grep _www:)" ]; then |  | ||||||
|   # Mac |  | ||||||
|   my_user=_www |  | ||||||
|   my_group=_www |  | ||||||
| else |  | ||||||
|   # Unsure |  | ||||||
|   my_user=$(whoami) |  | ||||||
|   my_group=$(id -g -n) |  | ||||||
| fi |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| # Not every platform has or needs sudo, gotta save them O(1)s... |  | ||||||
| sudo_cmd="" |  | ||||||
| set +e |  | ||||||
| if type -p sudo >/dev/null 2>/dev/null; then |  | ||||||
|   ((EUID)) && [[ -z "${ANDROID_ROOT-}" ]] && sudo_cmd="sudo" |  | ||||||
| fi |  | ||||||
| set -e |  | ||||||
| @ -1,585 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.dependencies = [ 'OAUTH3', 'storage.owners', 'options.device' ]; |  | ||||||
| module.exports.create = function (deps, conf) { |  | ||||||
|   var scmp = require('scmp'); |  | ||||||
|   var crypto = require('crypto'); |  | ||||||
|   var jwt = require('jsonwebtoken'); |  | ||||||
|   var bodyParser = require('body-parser'); |  | ||||||
|   var jsonParser = bodyParser.json({ |  | ||||||
|     inflate: true, limit: '100kb', reviver: null, strict: true /* type, verify */ |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   function handleCors(req, res, methods) { |  | ||||||
|     if (!methods) { |  | ||||||
|       methods = ['GET', 'POST']; |  | ||||||
|     } |  | ||||||
|     if (!Array.isArray(methods)) { |  | ||||||
|       methods = [ methods ]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*'); |  | ||||||
|     res.setHeader('Access-Control-Allow-Methods', methods.join(', ')); |  | ||||||
|     res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); |  | ||||||
|     res.setHeader('Access-Control-Allow-Credentials', 'true'); |  | ||||||
| 
 |  | ||||||
|     if (req.method.toUpperCase() === 'OPTIONS') { |  | ||||||
|       res.setHeader('Allow', methods.join(', ')); |  | ||||||
|       res.end(); |  | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (methods.indexOf('*') >= 0) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     if (methods.indexOf(req.method.toUpperCase()) < 0) { |  | ||||||
|       res.statusCode = 405; |  | ||||||
|       res.setHeader('Content-Type', 'application/json'); |  | ||||||
|       res.end(JSON.stringify({ error: { message: 'method '+req.method+' not allowed', code: 'EBADMETHOD'}})); |  | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   function makeCorsHandler(methods) { |  | ||||||
|     return function corsHandler(req, res, next) { |  | ||||||
|       if (!handleCors(req, res, methods)) { |  | ||||||
|         next(); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function handlePromise(req, res, prom) { |  | ||||||
|     prom.then(function (result) { |  | ||||||
|       res.send(deps.recase.snakeCopy(result)); |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       if (conf.debug) { |  | ||||||
|         console.log(err); |  | ||||||
|       } |  | ||||||
|       res.statusCode = err.statusCode || 500; |  | ||||||
|       err.message = err.message || err.toString(); |  | ||||||
|       res.end(JSON.stringify({error: {message: err.message, code: err.code}})); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function isAuthorized(req, res, fn) { |  | ||||||
|     var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, '')); |  | ||||||
|     if (!auth) { |  | ||||||
|       res.statusCode = 401; |  | ||||||
|       res.setHeader('Content-Type', 'application/json;'); |  | ||||||
|       res.end(JSON.stringify({ error: { message: "no token", code: 'E_NO_TOKEN', uri: undefined } })); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var id = crypto.createHash('sha256').update(auth.sub).digest('hex'); |  | ||||||
|     return deps.storage.owners.exists(id).then(function (exists) { |  | ||||||
|       if (!exists) { |  | ||||||
|         res.statusCode = 401; |  | ||||||
|         res.setHeader('Content-Type', 'application/json;'); |  | ||||||
|         res.end(JSON.stringify({ error: { message: "not authorized", code: 'E_NO_AUTHZ', uri: undefined } })); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       req.userId = id; |  | ||||||
|       fn(); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function checkPaywall() { |  | ||||||
|     var url = require('url'); |  | ||||||
|     var PromiseA = require('bluebird'); |  | ||||||
|     var testDomains = [ |  | ||||||
|       'daplie.com' |  | ||||||
|     , 'duckduckgo.com' |  | ||||||
|     , 'google.com' |  | ||||||
|     , 'amazon.com' |  | ||||||
|     , 'facebook.com' |  | ||||||
|     , 'msn.com' |  | ||||||
|     , 'yahoo.com' |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     // While this is not being developed behind a paywall the current idea is that
 |  | ||||||
|     // a paywall will either manipulate DNS queries to point to the paywall gate,
 |  | ||||||
|     // or redirect HTTP requests to the paywall gate. So we check for both and
 |  | ||||||
|     // hope we can detect most hotel/ISP paywalls out there in the world.
 |  | ||||||
|     //
 |  | ||||||
|     // It is also possible that the paywall will prevent any unknown traffic from
 |  | ||||||
|     // leaving the network, so the DNS queries could fail if the unit is set to
 |  | ||||||
|     // use nameservers other than the paywall router.
 |  | ||||||
|     return PromiseA.resolve() |  | ||||||
|     .then(function () { |  | ||||||
|       var dns = PromiseA.promisifyAll(require('dns')); |  | ||||||
|       var proms = testDomains.map(function (dom) { |  | ||||||
|         return dns.resolve6Async(dom) |  | ||||||
|           .catch(function () { |  | ||||||
|             return dns.resolve4Async(dom); |  | ||||||
|           }) |  | ||||||
|           .then(function (result) { |  | ||||||
|             return result[0]; |  | ||||||
|           }, function () { |  | ||||||
|             return null; |  | ||||||
|           }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       return PromiseA.all(proms).then(function (addrs) { |  | ||||||
|         var unique = addrs.filter(function (value, ind, self) { |  | ||||||
|           return value && self.indexOf(value) === ind; |  | ||||||
|         }); |  | ||||||
|         // It is possible some walls might have exceptions that leave some of the domains
 |  | ||||||
|         // we test alone, so we might have more than one unique address even behind an
 |  | ||||||
|         // active paywall.
 |  | ||||||
|         return unique.length < addrs.length; |  | ||||||
|       }); |  | ||||||
|     }) |  | ||||||
|     .then(function (paywall) { |  | ||||||
|       if (paywall) { |  | ||||||
|         return paywall; |  | ||||||
|       } |  | ||||||
|       var request = deps.request.defaults({ |  | ||||||
|         followRedirect: false |  | ||||||
|       , headers: { |  | ||||||
|           connection: 'close' |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       var proms = testDomains.map(function (dom) { |  | ||||||
|         return request('http://'+dom).then(function (resp) { |  | ||||||
|           if (resp.statusCode >= 300 && resp.statusCode < 400) { |  | ||||||
|             return url.parse(resp.headers.location).hostname; |  | ||||||
|           } else { |  | ||||||
|             return dom; |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       return PromiseA.all(proms).then(function (urls) { |  | ||||||
|         var unique = urls.filter(function (value, ind, self) { |  | ||||||
|           return value && self.indexOf(value) === ind; |  | ||||||
|         }); |  | ||||||
|         return unique.length < urls.length; |  | ||||||
|       }); |  | ||||||
|     }) |  | ||||||
|     ; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // This object contains all of the API endpoints written before we changed how
 |  | ||||||
|   // the API routing is handled. Eventually it will hopefully disappear, but for
 |  | ||||||
|   // now we're focusing on the things that need changing more.
 |  | ||||||
|   var oldEndPoints = { |  | ||||||
|     init: function (req, res) { |  | ||||||
|       if (handleCors(req, res, ['GET', 'POST'])) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if ('POST' !== req.method) { |  | ||||||
|         // It should be safe to give the list of owner IDs to an un-authenticated
 |  | ||||||
|         // request because the ID is the sha256 of the PPID and shouldn't be reversible
 |  | ||||||
|         return deps.storage.owners.all().then(function (results) { |  | ||||||
|           var ids = results.map(function (owner) { |  | ||||||
|             return owner.id; |  | ||||||
|           }); |  | ||||||
|           res.setHeader('Content-Type', 'application/json'); |  | ||||||
|           res.end(JSON.stringify(ids)); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       jsonParser(req, res, function () { |  | ||||||
| 
 |  | ||||||
|       return deps.PromiseA.resolve().then(function () { |  | ||||||
|         console.log('init POST body', req.body); |  | ||||||
| 
 |  | ||||||
|         var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, '')); |  | ||||||
|         var token = jwt.decode(req.body.access_token); |  | ||||||
|         var refresh = jwt.decode(req.body.refresh_token); |  | ||||||
|         auth.sub = auth.sub || auth.acx.id; |  | ||||||
|         token.sub = token.sub || token.acx.id; |  | ||||||
|         refresh.sub = refresh.sub || refresh.acx.id; |  | ||||||
| 
 |  | ||||||
|         // TODO validate token with issuer, but as-is the sub is already a secret
 |  | ||||||
|         var id = crypto.createHash('sha256').update(auth.sub).digest('hex'); |  | ||||||
|         var tid = crypto.createHash('sha256').update(token.sub).digest('hex'); |  | ||||||
|         var rid = crypto.createHash('sha256').update(refresh.sub).digest('hex'); |  | ||||||
|         var session = { |  | ||||||
|           access_token: req.body.access_token |  | ||||||
|         , token: token |  | ||||||
|         , refresh_token: req.body.refresh_token |  | ||||||
|         , refresh: refresh |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         console.log('ids', id, tid, rid); |  | ||||||
| 
 |  | ||||||
|         if (req.body.ip_url) { |  | ||||||
|           // TODO set options / GunDB
 |  | ||||||
|           conf.ip_url = req.body.ip_url; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return deps.storage.owners.all().then(function (results) { |  | ||||||
|           console.log('results', results); |  | ||||||
|           var err; |  | ||||||
| 
 |  | ||||||
|           // There is no owner yet. First come, first serve.
 |  | ||||||
|           if (!results || !results.length) { |  | ||||||
|             if (tid !== id || rid !== id) { |  | ||||||
|               err = new Error( |  | ||||||
|                 "When creating an owner the Authorization Bearer and Token and Refresh must all match" |  | ||||||
|               ); |  | ||||||
|               err.statusCode = 400; |  | ||||||
|               return deps.PromiseA.reject(err); |  | ||||||
|             } |  | ||||||
|             console.log('no owner, creating'); |  | ||||||
|             return deps.storage.owners.set(id, session); |  | ||||||
|           } |  | ||||||
|           console.log('has results'); |  | ||||||
| 
 |  | ||||||
|           // There are onwers. Is this one of them?
 |  | ||||||
|           if (!results.some(function (token) { |  | ||||||
|             return scmp(id, token.id); |  | ||||||
|           })) { |  | ||||||
|             err = new Error("Authorization token does not belong to an existing owner."); |  | ||||||
|             err.statusCode = 401; |  | ||||||
|             return deps.PromiseA.reject(err); |  | ||||||
|           } |  | ||||||
|           console.log('has correct owner'); |  | ||||||
| 
 |  | ||||||
|           // We're adding an owner, unless it already exists
 |  | ||||||
|           if (!results.some(function (token) { |  | ||||||
|             return scmp(tid, token.id); |  | ||||||
|           })) { |  | ||||||
|             console.log('adds new owner with existing owner'); |  | ||||||
|             return deps.storage.owners.set(tid, session); |  | ||||||
|           } |  | ||||||
|         }).then(function () { |  | ||||||
|           res.setHeader('Content-Type', 'application/json;'); |  | ||||||
|           res.end(JSON.stringify({ success: true })); |  | ||||||
|         }); |  | ||||||
|       }) |  | ||||||
|       .catch(function (err) { |  | ||||||
|         res.setHeader('Content-Type', 'application/json;'); |  | ||||||
|         res.statusCode = err.statusCode || 500; |  | ||||||
|         res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } })); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , request: function (req, res) { |  | ||||||
|       if (handleCors(req, res, '*')) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       isAuthorized(req, res, function () { |  | ||||||
|       jsonParser(req, res, function () { |  | ||||||
| 
 |  | ||||||
|         deps.request({ |  | ||||||
|           method: req.body.method || 'GET' |  | ||||||
|         , url: req.body.url |  | ||||||
|         , headers: req.body.headers |  | ||||||
|         , body: req.body.data |  | ||||||
|         }).then(function (resp) { |  | ||||||
|           if (resp.body instanceof Buffer || 'string' === typeof resp.body) { |  | ||||||
|             resp.body = JSON.parse(resp.body); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           return { |  | ||||||
|             statusCode: resp.statusCode |  | ||||||
|           , status: resp.status |  | ||||||
|           , headers: resp.headers |  | ||||||
|           , body: resp.body |  | ||||||
|           , data: resp.data |  | ||||||
|           }; |  | ||||||
|         }).then(function (result) { |  | ||||||
|           res.send(result); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|       }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , paywall_check: function (req, res) { |  | ||||||
|       if (handleCors(req, res, 'GET')) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       isAuthorized(req, res, function () { |  | ||||||
|         res.setHeader('Content-Type', 'application/json;'); |  | ||||||
| 
 |  | ||||||
|         checkPaywall().then(function (paywall) { |  | ||||||
|           res.end(JSON.stringify({paywall: paywall})); |  | ||||||
|         }, function (err) { |  | ||||||
|           err.message = err.message || err.toString(); |  | ||||||
|           res.statusCode = 500; |  | ||||||
|           res.end(JSON.stringify({error: {message: err.message, code: err.code}})); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , socks5: function (req, res) { |  | ||||||
|       if (handleCors(req, res, ['GET', 'POST', 'DELETE'])) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       isAuthorized(req, res, function () { |  | ||||||
|         var method = req.method.toUpperCase(); |  | ||||||
|         var prom; |  | ||||||
| 
 |  | ||||||
|         if (method === 'POST') { |  | ||||||
|           prom = deps.socks5.start(); |  | ||||||
|         } else if (method === 'DELETE') { |  | ||||||
|           prom = deps.socks5.stop(); |  | ||||||
|         } else { |  | ||||||
|           prom = deps.socks5.curState(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         res.setHeader('Content-Type', 'application/json;'); |  | ||||||
|         prom.then(function (result) { |  | ||||||
|           res.end(JSON.stringify(result)); |  | ||||||
|         }, function (err) { |  | ||||||
|           err.message = err.message || err.toString(); |  | ||||||
|           res.statusCode = 500; |  | ||||||
|           res.end(JSON.stringify({error: {message: err.message, code: err.code}})); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   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; |  | ||||||
| }; |  | ||||||
| @ -1,398 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var validator = new (require('jsonschema').Validator)(); |  | ||||||
| var recase = require('recase').create({}); |  | ||||||
| 
 |  | ||||||
| var portSchema = { type: 'number', minimum: 1, maximum: 65535 }; |  | ||||||
| 
 |  | ||||||
| var moduleSchemas = { |  | ||||||
|   // the proxy module is common to basically all categories.
 |  | ||||||
|   proxy: { |  | ||||||
|     type: 'object' |  | ||||||
|   , oneOf: [ |  | ||||||
|       { required: [ 'address' ] } |  | ||||||
|     , { required: [ 'port' ] } |  | ||||||
|     ] |  | ||||||
|   , properties: { |  | ||||||
|       address: { type: 'string' } |  | ||||||
|     , host:    { type: 'string' } |  | ||||||
|     , port:    portSchema |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // redirect and static modules are for HTTP
 |  | ||||||
| , redirect: { |  | ||||||
|     type: 'object' |  | ||||||
|   , required: [ 'to', 'from' ] |  | ||||||
|   , properties: { |  | ||||||
|       to:     { type: 'string'} |  | ||||||
|     , from:   { type: 'string'} |  | ||||||
|     , status: { type: 'integer', minimum: 1, maximum: 999 } |  | ||||||
|   , } |  | ||||||
|   } |  | ||||||
| , static: { |  | ||||||
|     type: 'object' |  | ||||||
|   , required: [ 'root' ] |  | ||||||
|   , properties: { |  | ||||||
|       root: { type: 'string' } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // the acme module is for TLS
 |  | ||||||
| , acme: { |  | ||||||
|     type: 'object' |  | ||||||
|   , required: [ 'email' ] |  | ||||||
|   , properties: { |  | ||||||
|       email:          { type: 'string' } |  | ||||||
|     , server:         { type: 'string' } |  | ||||||
|     , challenge_type: { type: 'string' } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // the dns control modules for DDNS
 |  | ||||||
| , 'dns@oauth3.org': { |  | ||||||
|     type: 'object' |  | ||||||
|   , required: [ 'token_id' ] |  | ||||||
|   , properties: { |  | ||||||
|       token_id: { type: 'string' } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| // forward is basically the same as proxy, but specifies the relevant incoming port(s).
 |  | ||||||
| // only allows for the raw transport layers (TCP/UDP)
 |  | ||||||
| moduleSchemas.forward = JSON.parse(JSON.stringify(moduleSchemas.proxy)); |  | ||||||
| moduleSchemas.forward.required = [ 'ports' ]; |  | ||||||
| moduleSchemas.forward.properties.ports = { type: 'array', items: portSchema }; |  | ||||||
| 
 |  | ||||||
| Object.keys(moduleSchemas).forEach(function (name) { |  | ||||||
|   var schema = moduleSchemas[name]; |  | ||||||
|   schema.id = '/modules/'+name; |  | ||||||
|   schema.required = ['id', 'type'].concat(schema.required || []); |  | ||||||
|   schema.properties.id   = { type: 'string' }; |  | ||||||
|   schema.properties.type = { type: 'string', const: name }; |  | ||||||
|   validator.addSchema(schema, schema.id); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| function addDomainRequirement(itemSchema) { |  | ||||||
|   var result = Object.assign({}, itemSchema); |  | ||||||
|   result.required = (result.required || []).concat('domains'); |  | ||||||
|   result.properties = Object.assign({}, result.properties); |  | ||||||
|   result.properties.domains = { type: 'array', items: { type: 'string' }, minLength: 1}; |  | ||||||
|   return result; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function toSchemaRef(name) { |  | ||||||
|   return { '$ref': '/modules/'+name }; |  | ||||||
| } |  | ||||||
| var moduleRefs = { |  | ||||||
|   http: [ 'proxy', 'static', 'redirect' ].map(toSchemaRef) |  | ||||||
| , tls:  [ 'proxy', 'acme' ].map(toSchemaRef) |  | ||||||
| , tcp:  [ 'forward' ].map(toSchemaRef) |  | ||||||
| , udp:  [ 'forward' ].map(toSchemaRef) |  | ||||||
| , ddns: [ 'dns@oauth3.org' ].map(toSchemaRef) |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // TCP is a bit special in that it has a module that doesn't operate based on domain name
 |  | ||||||
| // (ie forward), and a modules that does (ie proxy). It therefore has different module
 |  | ||||||
| // when part of the `domains` config, and when not part of the `domains` config the proxy
 |  | ||||||
| // modules must have the `domains` property while forward should not have it.
 |  | ||||||
| moduleRefs.tcp.push(addDomainRequirement(toSchemaRef('proxy'))); |  | ||||||
| 
 |  | ||||||
| var domainSchema = { |  | ||||||
|   type: 'array' |  | ||||||
| , items: { |  | ||||||
|     type: 'object' |  | ||||||
|   , properties: { |  | ||||||
|       id:      { type: 'string' } |  | ||||||
|     , names:   { type: 'array', items: { type: 'string' }, minLength: 1} |  | ||||||
|     , modules: { |  | ||||||
|         type: 'object' |  | ||||||
|       , properties: { |  | ||||||
|           tls:  { type: 'array', items: { oneOf: moduleRefs.tls }} |  | ||||||
|         , http: { type: 'array', items: { oneOf: moduleRefs.http }} |  | ||||||
|         , ddns: { type: 'array', items: { oneOf: moduleRefs.ddns }} |  | ||||||
|         , tcp:  { type: 'array', items: { oneOf: ['proxy'].map(toSchemaRef)}} |  | ||||||
|         } |  | ||||||
|       , additionalProperties: false |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var httpSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , properties: { |  | ||||||
|     modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.http }) } |  | ||||||
| 
 |  | ||||||
|     // These properties should be snake_case to match the API and config format
 |  | ||||||
|   , primary_domain: { type: 'string' } |  | ||||||
|   , allow_insecure: { type: 'boolean' } |  | ||||||
|   , trust_proxy:    { type: 'boolean' } |  | ||||||
| 
 |  | ||||||
|     // these are forbidden deprecated settings.
 |  | ||||||
|   , bind:    { not: {} } |  | ||||||
|   , domains: { not: {} } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var tlsSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , properties: { |  | ||||||
|     modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.tls }) } |  | ||||||
| 
 |  | ||||||
|     // these are forbidden deprecated settings.
 |  | ||||||
|   , acme:    { not: {} } |  | ||||||
|   , bind:    { not: {} } |  | ||||||
|   , domains: { not: {} } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var tcpSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , required: [ 'bind' ] |  | ||||||
| , properties: { |  | ||||||
|     bind:    { type: 'array', items: portSchema, minLength: 1 } |  | ||||||
|   , modules: { type: 'array', items: { oneOf: moduleRefs.tcp }} |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var udpSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , properties: { |  | ||||||
|     bind:    { type: 'array', items: portSchema } |  | ||||||
|   , modules: { type: 'array', items: { oneOf: moduleRefs.udp }} |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var mdnsSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , required: [ 'port', 'broadcast', 'ttl' ] |  | ||||||
| , properties: { |  | ||||||
|     port:      portSchema |  | ||||||
|   , broadcast: { type: 'string' } |  | ||||||
|   , ttl:       { type: 'integer', minimum: 0, maximum: 2147483647 } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var tunnelSvrSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , properties: { |  | ||||||
|     servernames: { type: 'array', items: { type: 'string' }} |  | ||||||
|   , secret:      { type: 'string' } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var ddnsSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , properties: { |  | ||||||
|     loopback: { |  | ||||||
|       type: 'object' |  | ||||||
|     , required: [ 'type', 'domain' ] |  | ||||||
|     , properties: { |  | ||||||
|         type:   { type: 'string', const: 'tunnel@oauth3.org' } |  | ||||||
|       , domain: { type: 'string'} |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   , tunnel: { |  | ||||||
|       type: 'object' |  | ||||||
|     , required: [ 'type', 'token_id' ] |  | ||||||
|     , properties: { |  | ||||||
|         type:  { type: 'string', const: 'tunnel@oauth3.org' } |  | ||||||
|       , token_id: { type: 'string'} |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   , modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.ddns })} |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| var socks5Schema = { |  | ||||||
|   type: 'object' |  | ||||||
| , properties: { |  | ||||||
|     enabled: { type: 'boolean' } |  | ||||||
|   , port:    portSchema |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| var deviceSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , properties: { |  | ||||||
|     hostname: { type: 'string' } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var mainSchema = { |  | ||||||
|   type: 'object' |  | ||||||
| , required: [ 'domains', 'http', 'tls', 'tcp', 'udp', 'mdns', 'ddns' ] |  | ||||||
| , properties: { |  | ||||||
|     domains:domainSchema |  | ||||||
|   , http:   httpSchema |  | ||||||
|   , tls:    tlsSchema |  | ||||||
|   , tcp:    tcpSchema |  | ||||||
|   , udp:    udpSchema |  | ||||||
|   , mdns:   mdnsSchema |  | ||||||
|   , ddns:   ddnsSchema |  | ||||||
|   , socks5: socks5Schema |  | ||||||
|   , device: deviceSchema |  | ||||||
|   , tunnel_server: tunnelSvrSchema |  | ||||||
|   } |  | ||||||
| , additionalProperties: false |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function validate(config) { |  | ||||||
|   return validator.validate(recase.snakeCopy(config), mainSchema).errors; |  | ||||||
| } |  | ||||||
| module.exports.validate = validate; |  | ||||||
| 
 |  | ||||||
| class IdList extends Array { |  | ||||||
|   constructor(rawList) { |  | ||||||
|     super(); |  | ||||||
|     if (Array.isArray(rawList)) { |  | ||||||
|       Object.assign(this, JSON.parse(JSON.stringify(rawList))); |  | ||||||
|     } |  | ||||||
|     this._itemName = 'item'; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   findId(id) { |  | ||||||
|     return Array.prototype.find.call(this, function (dom) { |  | ||||||
|       return dom.id === id; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   add(item) { |  | ||||||
|     item.id = require('crypto').randomBytes(4).toString('hex'); |  | ||||||
|     this.push(item); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   update(id, update) { |  | ||||||
|     var item = this.findId(id); |  | ||||||
|     if (!item) { |  | ||||||
|       var error = new Error("no "+this._itemName+" with ID '"+id+"'"); |  | ||||||
|       error.statusCode = 404; |  | ||||||
|       throw error; |  | ||||||
|     } |  | ||||||
|     Object.assign(this.findId(id), update); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   remove(id) { |  | ||||||
|     var index = this.findIndex(function (dom) { |  | ||||||
|       return dom.id === id; |  | ||||||
|     }); |  | ||||||
|     if (index < 0) { |  | ||||||
|       var error = new Error("no "+this._itemName+" with ID '"+id+"'"); |  | ||||||
|       error.statusCode = 404; |  | ||||||
|       throw error; |  | ||||||
|     } |  | ||||||
|     this.splice(index, 1); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| class ModuleList extends IdList { |  | ||||||
|   constructor(rawList) { |  | ||||||
|     super(rawList); |  | ||||||
|     this._itemName = 'module'; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   add(mod) { |  | ||||||
|     if (!mod.type) { |  | ||||||
|       throw new Error("module must have a 'type' defined"); |  | ||||||
|     } |  | ||||||
|     if (!moduleSchemas[mod.type]) { |  | ||||||
|       throw new Error("invalid module type '"+mod.type+"'"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     mod.id = require('crypto').randomBytes(4).toString('hex'); |  | ||||||
|     this.push(mod); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| class DomainList extends IdList { |  | ||||||
|   constructor(rawList) { |  | ||||||
|     super(rawList); |  | ||||||
|     this._itemName = 'domain'; |  | ||||||
|     this.forEach(function (dom) { |  | ||||||
|       dom.modules = { |  | ||||||
|         http: new ModuleList((dom.modules || {}).http) |  | ||||||
|       , tls:  new ModuleList((dom.modules || {}).tls) |  | ||||||
|       , ddns: new ModuleList((dom.modules || {}).ddns) |  | ||||||
|       , tcp:  new ModuleList((dom.modules || {}).tcp) |  | ||||||
|       }; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   add(dom) { |  | ||||||
|     if (!Array.isArray(dom.names) || !dom.names.length) { |  | ||||||
|       throw new Error("domains must have a non-empty array for 'names'"); |  | ||||||
|     } |  | ||||||
|     if (dom.names.some(function (name) { return typeof name !== 'string'; })) { |  | ||||||
|       throw new Error("all domain names must be strings"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var modLists = { |  | ||||||
|       http: new ModuleList() |  | ||||||
|     , tls:  new ModuleList() |  | ||||||
|     , ddns: new ModuleList() |  | ||||||
|     , tcp:  new ModuleList() |  | ||||||
|     }; |  | ||||||
|     // We add these after instead of in the constructor to run the validation and manipulation
 |  | ||||||
|     // in the ModList add function since these are all new modules.
 |  | ||||||
|     if (dom.modules) { |  | ||||||
|       Object.keys(modLists).forEach(function (key) { |  | ||||||
|         if (Array.isArray(dom.modules[key])) { |  | ||||||
|           dom.modules[key].forEach(modLists[key].add, modLists[key]); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     dom.id = require('crypto').randomBytes(4).toString('hex'); |  | ||||||
|     dom.modules = modLists; |  | ||||||
|     this.push(dom); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class ConfigChanger { |  | ||||||
|   constructor(start) { |  | ||||||
|     Object.assign(this, JSON.parse(JSON.stringify(start))); |  | ||||||
|     delete this.device; |  | ||||||
|     delete this.debug; |  | ||||||
| 
 |  | ||||||
|     this.domains = new DomainList(this.domains); |  | ||||||
|     this.http.modules = new ModuleList(this.http.modules); |  | ||||||
|     this.tls.modules  = new ModuleList(this.tls.modules); |  | ||||||
|     this.tcp.modules  = new ModuleList(this.tcp.modules); |  | ||||||
|     this.udp.modules  = new ModuleList(this.udp.modules); |  | ||||||
|     this.ddns.modules = new ModuleList(this.ddns.modules); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   update(update) { |  | ||||||
|     var self = this; |  | ||||||
| 
 |  | ||||||
|     if (update.domains) { |  | ||||||
|       update.domains.forEach(self.domains.add, self.domains); |  | ||||||
|     } |  | ||||||
|     [ 'http', 'tls', 'tcp', 'udp', 'ddns' ].forEach(function (name) { |  | ||||||
|       if (update[name] && update[name].modules) { |  | ||||||
|         update[name].modules.forEach(self[name].modules.add, self[name].modules); |  | ||||||
|         delete update[name].modules; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     function mergeSettings(orig, changes) { |  | ||||||
|       Object.keys(changes).forEach(function (key) { |  | ||||||
|         // TODO: use an API that can properly handle updating arrays.
 |  | ||||||
|         if (!changes[key] || (typeof changes[key] !== 'object') || Array.isArray(changes[key])) { |  | ||||||
|           orig[key] = changes[key]; |  | ||||||
|         } |  | ||||||
|         else if (!orig[key] || typeof orig[key] !== 'object') { |  | ||||||
|           orig[key] = changes[key]; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|           mergeSettings(orig[key], changes[key]); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     mergeSettings(this, update); |  | ||||||
| 
 |  | ||||||
|     return validate(this); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   validate() { |  | ||||||
|     return validate(this); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| module.exports.ConfigChanger = ConfigChanger; |  | ||||||
| @ -1,31 +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 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); |  | ||||||
| }; |  | ||||||
							
								
								
									
										440
									
								
								lib/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										440
									
								
								lib/app.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,440 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports = function (opts) { | ||||||
|  |   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 = opts.content; | ||||||
|  |   //var server;
 | ||||||
|  |   var serveInit; | ||||||
|  |   var app; | ||||||
|  | 
 | ||||||
|  |   function _reloadWrite(data, enc, cb) { | ||||||
|  |     /*jshint validthis: true */ | ||||||
|  |     if (this.headersSent) { | ||||||
|  |       this.__write(data, enc, cb); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!/html/i.test(this.getHeader('Content-Type'))) { | ||||||
|  |       this.__write(data, enc, cb); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this.getHeader('Content-Length')) { | ||||||
|  |       this.setHeader('Content-Length', this.getHeader('Content-Length') + this.__my_addLen); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.__write(this.__my_livereload); | ||||||
|  |     this.__write(data, enc, cb); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function createServeInit() { | ||||||
|  |     var PromiseA = require('bluebird'); | ||||||
|  |     var stunnel = require('stunnel'); | ||||||
|  |     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'); | ||||||
|  |     var fs = PromiseA.promisifyAll(require('fs')); | ||||||
|  |     var ownersPath = path.join(__dirname, '..', 'var', 'owners.json'); | ||||||
|  | 
 | ||||||
|  |     var scmp = require('scmp'); | ||||||
|  | 
 | ||||||
|  |     return require('../packages/apis/com.daplie.caddy').create({ | ||||||
|  |       PromiseA: PromiseA | ||||||
|  |     , OAUTH3: OAUTH3 | ||||||
|  |     , storage: { | ||||||
|  |         owners: { | ||||||
|  |           all: function () { | ||||||
|  |             var owners; | ||||||
|  |             try { | ||||||
|  |               owners = require(ownersPath); | ||||||
|  |             } catch(e) { | ||||||
|  |               owners = {}; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return PromiseA.resolve(Object.keys(owners).map(function (key) { | ||||||
|  |               var owner = owners[key]; | ||||||
|  |               owner.id = key; | ||||||
|  |               return owner; | ||||||
|  |             })); | ||||||
|  |           } | ||||||
|  |         , get: function (id) { | ||||||
|  |             var me = this; | ||||||
|  | 
 | ||||||
|  |             return me.all().then(function (owners) { | ||||||
|  |               return owners.filter(function (owner) { | ||||||
|  |                 return scmp(id, owner.id); | ||||||
|  |               })[0]; | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         , exists: function (id) { | ||||||
|  |             var me = this; | ||||||
|  | 
 | ||||||
|  |             return me.get(id).then(function (owner) { | ||||||
|  |               return !!owner; | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         , set: function (id, obj) { | ||||||
|  |             var owners; | ||||||
|  |             try { | ||||||
|  |               owners = require(ownersPath); | ||||||
|  |             } catch(e) { | ||||||
|  |               owners = {}; | ||||||
|  |             } | ||||||
|  |             obj.id = id; | ||||||
|  |             owners[id] = obj; | ||||||
|  | 
 | ||||||
|  |             return fs.writeFileAsync(ownersPath, JSON.stringify(owners), 'utf8'); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     , recase: require('recase').create({}) | ||||||
|  |     , request: PromiseA.promisify(require('request')) | ||||||
|  |     , options: opts | ||||||
|  |     , api: { | ||||||
|  |         tunnel: function (deps, session) { | ||||||
|  |           var OAUTH3 = deps.OAUTH3; | ||||||
|  |           var url = require('url'); | ||||||
|  |           var providerUri = session.token.aud; | ||||||
|  |           var urlObj = url.parse(OAUTH3.url.normalize(session.token.azp)); | ||||||
|  |           var oauth3 = OAUTH3.create(urlObj, { | ||||||
|  |             providerUri: providerUri | ||||||
|  |           , session: session | ||||||
|  |           }); | ||||||
|  |           //var crypto = require('crypto');
 | ||||||
|  |           //var id = crypto.createHash('sha256').update(session.token.sub).digest('hex');
 | ||||||
|  |           return oauth3.setProvider(providerUri).then(function () { | ||||||
|  |             return oauth3.api('domains.list').then(function (domains) { | ||||||
|  |               var domainsMap = {}; | ||||||
|  |               domains.forEach(function (d) { | ||||||
|  |                 if (!d.device) { | ||||||
|  |                   return; | ||||||
|  |                 } | ||||||
|  |                 if (d.device !== deps.options.device.hostname) { | ||||||
|  |                   return; | ||||||
|  |                 } | ||||||
|  |                 domainsMap[d.name] = true; | ||||||
|  |               }); | ||||||
|  | 
 | ||||||
|  |               //console.log('domains matching hostname', Object.keys(domainsMap));
 | ||||||
|  |               //console.log('device', deps.options.device);
 | ||||||
|  |               return oauth3.api('tunnel.token', { | ||||||
|  |                 data: { | ||||||
|  |                   // filter to all domains that are on this device
 | ||||||
|  |                   domains: Object.keys(domainsMap) | ||||||
|  |                 , device: { | ||||||
|  |                     hostname: deps.options.device.hostname | ||||||
|  |                   , id: deps.options.device.uid || deps.options.device.id | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               }).then(function (result) { | ||||||
|  |                 console.log('got a token from the tunnel server?'); | ||||||
|  |                 console.log(result); | ||||||
|  |                 if (!result.tunnelUrl) { | ||||||
|  |                   result.tunnelUrl = ('wss://' + (new Buffer(results.jwt.split('.')[1], 'base64').toString('ascii')).aud + '/'); | ||||||
|  |                 } | ||||||
|  |                 var opts = { | ||||||
|  |                   token: results.jwt | ||||||
|  |                 , stunneld: results.tunnelUrl | ||||||
|  |                   // we'll provide faux networking and pipe as we please
 | ||||||
|  |                 , services: { https: { '*': 443 }, http: { '*': 80 }, smtp: { '*': 25}, smtps: { '*': 587 /*also 465/starttls*/ } /*, ssh: { '*': 22 }*/ } | ||||||
|  |                 , net: opts.net | ||||||
|  |                 }; | ||||||
|  |               }); | ||||||
|  |             }); | ||||||
|  |           }); | ||||||
|  |           //, { token: token, refresh: refresh });
 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   app = express(); | ||||||
|  | 
 | ||||||
|  |   if (!opts.sites) { | ||||||
|  |     opts.sites = []; | ||||||
|  |   } | ||||||
|  |   opts.sites._map = {}; | ||||||
|  |   opts.sites.forEach(function (site) { | ||||||
|  | 
 | ||||||
|  |     if (!opts.sites._map[site.$id]) { | ||||||
|  |       opts.sites._map[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; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   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); | ||||||
|  |   }); | ||||||
|  |   return app.use('/', function (req, res, next) { | ||||||
|  |     if (!req.headers.host) { | ||||||
|  |       next(new Error('missing HTTP Host header')); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (0 === req.url.indexOf('/api/com.daplie.caddy/')) { | ||||||
|  |       if (!serveInit) { | ||||||
|  |         serveInit = createServeInit(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if ('/api/com.daplie.caddy/init' === req.url) { | ||||||
|  |       serveInit.init(req, res); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if ('/api/com.daplie.caddy/tunnel' === req.url) { | ||||||
|  |       serveInit.tunnel(req, res); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if ('/api/com.daplie.caddy/config' === req.url) { | ||||||
|  |       serveInit.config(req, res); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if ('/api/com.daplie.caddy/request' === req.url) { | ||||||
|  |       serveInit.request(req, res); | ||||||
|  |       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(opts.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(opts.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); | ||||||
|  |         var app = require(appfile); | ||||||
|  |         app(req, res, 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); | ||||||
|  |     */ | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @ -1,54 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| function bindTcpAndRelease(port, cb) { |  | ||||||
|   var server = require('net').createServer(); |  | ||||||
|   server.on('error', function (e) { |  | ||||||
|     cb(e); |  | ||||||
|   }); |  | ||||||
|   server.listen(port, function () { |  | ||||||
|     server.close(); |  | ||||||
|     cb(); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function checkTcpPorts(cb) { |  | ||||||
|   var bound = {}; |  | ||||||
|   var failed = {}; |  | ||||||
| 
 |  | ||||||
|   bindTcpAndRelease(80, function (e) { |  | ||||||
|     if (e) { |  | ||||||
|       failed[80] = e; |  | ||||||
|       //console.log(e.code);
 |  | ||||||
|       //console.log(e.message);
 |  | ||||||
|     } else { |  | ||||||
|       bound['80'] = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     bindTcpAndRelease(443, function (e) { |  | ||||||
|       if (e) { |  | ||||||
|         failed[443] = e; |  | ||||||
|       } else { |  | ||||||
|         bound['443'] = true; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (bound['80'] && bound['443']) { |  | ||||||
|         cb(null, bound); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       console.warn("default ports 80 and 443 are not available, trying 8443"); |  | ||||||
| 
 |  | ||||||
|       bindTcpAndRelease(8443, function (e) { |  | ||||||
|         if (e) { |  | ||||||
|           failed[8443] = e; |  | ||||||
|         } else { |  | ||||||
|           bound['8443'] = true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         cb(failed, bound); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports.checkTcpPorts = checkTcpPorts; |  | ||||||
							
								
								
									
										88
									
								
								lib/ddns.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								lib/ddns.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (opts/*, servers*/) { | ||||||
|  |   var PromiseA = opts.PromiseA; | ||||||
|  |   var dns = PromiseA.promisifyAll(require('dns')); | ||||||
|  | 
 | ||||||
|  |   return PromiseA.all([ | ||||||
|  |     dns.resolve4Async(opts._old_server_name).then(function (results) { | ||||||
|  |       return results; | ||||||
|  |     }, function () {}) | ||||||
|  |   , dns.resolve6Async(opts._old_server_name).then(function (results) { | ||||||
|  |       return results; | ||||||
|  |     }, function () {}) | ||||||
|  |   ]).then(function (results) { | ||||||
|  |     var ipv4 = results[0] || []; | ||||||
|  |     var ipv6 = results[1] || []; | ||||||
|  |     var record; | ||||||
|  | 
 | ||||||
|  |     opts.dnsRecords = { | ||||||
|  |       A: ipv4 | ||||||
|  |     , AAAA: ipv6 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     Object.keys(opts.ifaces).some(function (ifacename) { | ||||||
|  |       var iface = opts.ifaces[ifacename]; | ||||||
|  | 
 | ||||||
|  |       return iface.ipv4.some(function (localIp) { | ||||||
|  |         return ipv4.some(function (remoteIp) { | ||||||
|  |           if (localIp.address === remoteIp) { | ||||||
|  |             record = localIp; | ||||||
|  |             return record; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }) || iface.ipv6.some(function (localIp) { | ||||||
|  |         return ipv6.forEach(function (remoteIp) { | ||||||
|  |           if (localIp.address === remoteIp) { | ||||||
|  |             record = localIp; | ||||||
|  |             return record; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (!record) { | ||||||
|  |       console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address."); | ||||||
|  |       console.info("Use --ddns to allow the people of the Internet to access your server."); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     opts.externalIps.ipv4.some(function (localIp) { | ||||||
|  |       return ipv4.some(function (remoteIp) { | ||||||
|  |         if (localIp.address === remoteIp) { | ||||||
|  |           record = localIp; | ||||||
|  |           return record; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     opts.externalIps.ipv6.some(function (localIp) { | ||||||
|  |       return ipv6.some(function (remoteIp) { | ||||||
|  |         if (localIp.address === remoteIp) { | ||||||
|  |           record = localIp; | ||||||
|  |           return record; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (!record) { | ||||||
|  |       console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address."); | ||||||
|  |       console.info("Use --ddns to allow the people of the Internet to access your server."); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | if (require.main === module) { | ||||||
|  |   var opts = { | ||||||
|  |     _old_server_name: 'aj.daplie.me' | ||||||
|  |   , PromiseA: require('bluebird') | ||||||
|  |   }; | ||||||
|  |   // ifaces
 | ||||||
|  |   opts.ifaces = require('./local-ip.js').find(); | ||||||
|  |   console.log('opts.ifaces'); | ||||||
|  |   console.log(opts.ifaces); | ||||||
|  |   require('./match-ips.js').match(opts._old_server_name, opts).then(function (ips) { | ||||||
|  |     opts.matchingIps = ips.matchingIps || []; | ||||||
|  |     opts.externalIps = ips.externalIps; | ||||||
|  |     module.exports.create(opts); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @ -1,122 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| // Much of this file was based on the `le-challenge-ddns` library (which we are not using
 |  | ||||||
| // here because it's method of setting records requires things we don't really want).
 |  | ||||||
| module.exports.create = function (deps, conf, utils) { |  | ||||||
| 
 |  | ||||||
|   function getReleventSessionId(domain) { |  | ||||||
|     var sessId; |  | ||||||
| 
 |  | ||||||
|     utils.iterateAllModules(function (mod, domainList) { |  | ||||||
|       // We return a truthy value in these cases because of the way the iterate function
 |  | ||||||
|       // handles modules grouped by domain. By returning true we are saying these domains
 |  | ||||||
|       // are "handled" and so if there are multiple modules we won't be given the rest.
 |  | ||||||
|       if (sessId) { return true; } |  | ||||||
|       if (domainList.indexOf(domain) < 0) { return true; } |  | ||||||
| 
 |  | ||||||
|       // But if the domains are relevant but we don't know how to handle the module we
 |  | ||||||
|       // return false to allow us to look at any other modules that might exist here.
 |  | ||||||
|       if (mod.type !== 'dns@oauth3.org')  { return false; } |  | ||||||
| 
 |  | ||||||
|       sessId = mod.tokenId || mod.token_id; |  | ||||||
|       return true; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return sessId; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function get(args, domain, challenge, done) { |  | ||||||
|     done(new Error("Challenge.get() does not need an implementation for dns-01. (did you mean Challenge.loopback?)")); |  | ||||||
|   } |  | ||||||
|   // same as get, but external
 |  | ||||||
|   function loopback(args, domain, challenge, done) { |  | ||||||
|     var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; |  | ||||||
|     require('dns').resolveTxt(challengeDomain, done); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var activeChallenges = {}; |  | ||||||
|   async function removeAsync(args, domain) { |  | ||||||
|     var data = activeChallenges[domain]; |  | ||||||
|     if (!data) { |  | ||||||
|       console.warn(new Error('cannot remove DNS challenge for ' + domain + ': already removed')); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var session = await utils.getSession(data.sessId); |  | ||||||
|     var directives = await deps.OAUTH3.discover(session.token.aud); |  | ||||||
|     var apiOpts = { |  | ||||||
|       api: 'dns.unset' |  | ||||||
|     , session: session |  | ||||||
|     , type: 'TXT' |  | ||||||
|     , value: data.keyAuthDigest |  | ||||||
|     }; |  | ||||||
|     await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, data.splitDomain)); |  | ||||||
| 
 |  | ||||||
|     delete activeChallenges[domain]; |  | ||||||
|   } |  | ||||||
|   async function setAsync(args, domain, challenge, keyAuth) { |  | ||||||
|     if (activeChallenges[domain]) { |  | ||||||
|       await removeAsync(args, domain, challenge); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var sessId = getReleventSessionId(domain); |  | ||||||
|     if (!sessId) { |  | ||||||
|       throw new Error('no DDNS module handles the domain ' + domain); |  | ||||||
|     } |  | ||||||
|     var session = await utils.getSession(sessId); |  | ||||||
|     var directives = await deps.OAUTH3.discover(session.token.aud); |  | ||||||
| 
 |  | ||||||
|     // I'm not sure what role challenge is supposed to play since even in the library
 |  | ||||||
|     // this code is based on it was never used, but check for it anyway because ...
 |  | ||||||
|     if (!challenge || keyAuth) { |  | ||||||
|       console.warn(new Error('DDNS challenge missing challenge or keyAuth')); |  | ||||||
|     } |  | ||||||
|     var keyAuthDigest = require('crypto').createHash('sha256').update(keyAuth || '').digest('base64') |  | ||||||
|       .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); |  | ||||||
| 
 |  | ||||||
|     var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; |  | ||||||
|     var splitDomain = (await utils.splitDomains(directives.api, [challengeDomain]))[0]; |  | ||||||
| 
 |  | ||||||
|     var apiOpts = { |  | ||||||
|       api: 'dns.set' |  | ||||||
|     , session: session |  | ||||||
|     , type: 'TXT' |  | ||||||
|     , value: keyAuthDigest |  | ||||||
|     , ttl: args.ttl || 0 |  | ||||||
|     }; |  | ||||||
|     await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, splitDomain)); |  | ||||||
| 
 |  | ||||||
|     activeChallenges[domain] = { |  | ||||||
|       sessId |  | ||||||
|     , keyAuthDigest |  | ||||||
|     , splitDomain |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     return new Promise(res => setTimeout(res, 1000)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // It might be slightly easier to use arguments and apply, but the library that will use
 |  | ||||||
|   // this function counts the arguments we expect.
 |  | ||||||
|   function set(a, b, c, d, done) { |  | ||||||
|     setAsync(a, b, c, d).then(result => done(null, result), done); |  | ||||||
|   } |  | ||||||
|   function remove(a, b, c, done) { |  | ||||||
|     removeAsync(a, b, c).then(result => done(null, result), done); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getOptions() { |  | ||||||
|     return { |  | ||||||
|       oauth3: 'oauth3.org' |  | ||||||
|     , debug: conf.debug |  | ||||||
|     , acmeChallengeDns: '_acme-challenge.' |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     getOptions |  | ||||||
|   , set |  | ||||||
|   , get |  | ||||||
|   , remove |  | ||||||
|   , loopback |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,132 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, conf, utils) { |  | ||||||
|   function dnsType(addr) { |  | ||||||
|     if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) { |  | ||||||
|       return 'A'; |  | ||||||
|     } |  | ||||||
|     if (-1 !== addr.indexOf(':') && /^[a-f:\.\d]+$/i.test(addr)) { |  | ||||||
|       return 'AAAA'; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function setDeviceAddress(session, addr, domains) { |  | ||||||
|     var directives = await deps.OAUTH3.discover(session.token.aud); |  | ||||||
| 
 |  | ||||||
|     // Set the address of the device to our public address.
 |  | ||||||
|     await deps.request({ |  | ||||||
|       url: deps.OAUTH3.url.normalize(directives.api)+'/api/com.daplie.domains/acl/devices/' + conf.device.hostname |  | ||||||
|     , method: 'POST' |  | ||||||
|     , headers: { |  | ||||||
|         'Authorization': 'Bearer ' + session.refresh_token |  | ||||||
|       , 'Accept': 'application/json; charset=utf-8' |  | ||||||
|       } |  | ||||||
|     , json: { |  | ||||||
|         addresses: [ |  | ||||||
|           { value: addr, type:  dnsType(addr) } |  | ||||||
|         ] |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Then update all of the records attached to our hostname, first removing the old records
 |  | ||||||
|     // to remove the reference to the old address, then creating new records for the same domains
 |  | ||||||
|     // using our new address.
 |  | ||||||
|     var allDns = await deps.OAUTH3.api(directives.api, {session: session, api: 'dns.list'}); |  | ||||||
|     var ourDns = allDns.filter(function (record) { |  | ||||||
|       if (record.device !== conf.device.hostname) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|       if ([ 'A', 'AAAA' ].indexOf(record.type) < 0) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|       return domains.indexOf(record.host) !== -1; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Of all the DNS records referring to our device and the current list of domains determine
 |  | ||||||
|     // which domains have records with outdated address, and which ones we can just leave be
 |  | ||||||
|     // without updating them.
 |  | ||||||
|     var badAddrDomains = ourDns.filter(function (record) { |  | ||||||
|       return record.value !== addr; |  | ||||||
|     }).map(record => record.host); |  | ||||||
|     var goodAddrDomains = ourDns.filter(function (record) { |  | ||||||
|       return record.value === addr && badAddrDomains.indexOf(record.host) < 0; |  | ||||||
|     }).map(record => record.host); |  | ||||||
|     var requiredUpdates = domains.filter(function (domain) { |  | ||||||
|       return goodAddrDomains.indexOf(domain) < 0; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     var oldDns = await utils.splitDomains(directives.api, badAddrDomains); |  | ||||||
|     var common = { |  | ||||||
|       api: 'devices.detach' |  | ||||||
|     , session: session |  | ||||||
|     , device: conf.device.hostname |  | ||||||
|     }; |  | ||||||
|     await deps.PromiseA.all(oldDns.map(function (record) { |  | ||||||
|       return deps.OAUTH3.api(directives.api, Object.assign({}, common, record)); |  | ||||||
|     })); |  | ||||||
|     if (conf.debug && badAddrDomains.length) { |  | ||||||
|       console.log('removed bad DNS records for ' + badAddrDomains.join(', ')); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var newDns = await utils.splitDomains(directives.api, requiredUpdates); |  | ||||||
|     common = { |  | ||||||
|       api: 'devices.attach' |  | ||||||
|     , session: session |  | ||||||
|     , device: conf.device.hostname |  | ||||||
|     , ip: addr |  | ||||||
|     , ttl: 300 |  | ||||||
|     }; |  | ||||||
|     await deps.PromiseA.all(newDns.map(function (record) { |  | ||||||
|       return deps.OAUTH3.api(directives.api, Object.assign({}, common, record)); |  | ||||||
|     })); |  | ||||||
|     if (conf.debug && requiredUpdates.length) { |  | ||||||
|       console.log('set new DNS records for ' + requiredUpdates.join(', ')); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function getDeviceAddresses(session) { |  | ||||||
|     var directives = await deps.OAUTH3.discover(session.token.aud); |  | ||||||
| 
 |  | ||||||
|     var result = await deps.request({ |  | ||||||
|       url: deps.OAUTH3.url.normalize(directives.api)+'/api/org.oauth3.dns/acl/devices' |  | ||||||
|     , method: 'GET' |  | ||||||
|     , headers: { |  | ||||||
|         'Authorization': 'Bearer ' + session.refresh_token |  | ||||||
|       , 'Accept': 'application/json; charset=utf-8' |  | ||||||
|       } |  | ||||||
|     , json: true |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     if (!result.body) { |  | ||||||
|       throw new Error('No response body in request for device addresses'); |  | ||||||
|     } |  | ||||||
|     if (result.body.error) { |  | ||||||
|       throw Object.assign(new Error('error getting device list'), result.body.error); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var dev = result.body.devices.filter(function (dev) { |  | ||||||
|       return dev.name === conf.device.hostname; |  | ||||||
|     })[0]; |  | ||||||
|     return (dev || {}).addresses || []; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function removeDomains(session, domains) { |  | ||||||
|     var directives = await deps.OAUTH3.discover(session.token.aud); |  | ||||||
| 
 |  | ||||||
|     var oldDns = await utils.splitDomains(directives.api, domains); |  | ||||||
|     var common = { |  | ||||||
|       api: 'devices.detach' |  | ||||||
|     , session: session |  | ||||||
|     , device: conf.device.hostname |  | ||||||
|     }; |  | ||||||
|     await deps.PromiseA.all(oldDns.map(function (record) { |  | ||||||
|       return deps.OAUTH3.api(directives.api, Object.assign({}, common, record)); |  | ||||||
|     })); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     getDeviceAddresses |  | ||||||
|   , setDeviceAddress |  | ||||||
|   , removeDomains |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,326 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, conf) { |  | ||||||
|   var dns = deps.PromiseA.promisifyAll(require('dns')); |  | ||||||
|   var network = deps.PromiseA.promisifyAll(deps.recase.camelCopy(require('network'))); |  | ||||||
|   var equal = require('deep-equal'); |  | ||||||
| 
 |  | ||||||
|   var utils = require('./utils').create(deps, conf); |  | ||||||
|   var loopback = require('./loopback').create(deps, conf, utils); |  | ||||||
|   var dnsCtrl = require('./dns-ctrl').create(deps, conf, utils); |  | ||||||
|   var challenge = require('./challenge-responder').create(deps, conf, utils); |  | ||||||
|   var tunnelClients = require('./tunnel-client-manager').create(deps, conf, utils); |  | ||||||
| 
 |  | ||||||
|   var loopbackDomain; |  | ||||||
| 
 |  | ||||||
|   var tunnelActive = false; |  | ||||||
|   async function startTunnel(tunnelSession, mod, domainList) { |  | ||||||
|     try { |  | ||||||
|       var dnsSession = await utils.getSession(mod.tokenId); |  | ||||||
|       var tunnelDomain = await tunnelClients.start(tunnelSession || dnsSession, domainList); |  | ||||||
| 
 |  | ||||||
|       var addrList; |  | ||||||
|       try { |  | ||||||
|         addrList = await dns.resolve4Async(tunnelDomain); |  | ||||||
|       } catch (e) {} |  | ||||||
|       if (!addrList || !addrList.length) { |  | ||||||
|         try { |  | ||||||
|           addrList = await dns.resolve6Async(tunnelDomain); |  | ||||||
|         } catch (e) {} |  | ||||||
|       } |  | ||||||
|       if (!addrList || !addrList.length || !addrList[0]) { |  | ||||||
|         throw new Error('failed to lookup IP for tunnel domain "' + tunnelDomain + '"'); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (!mod.disabled) { |  | ||||||
|         await dnsCtrl.setDeviceAddress(dnsSession, addrList[0], domainList); |  | ||||||
|       } |  | ||||||
|     } catch (err) { |  | ||||||
|       console.log('error starting tunnel for', domainList.join(', ')); |  | ||||||
|       console.log(err); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   async function connectAllTunnels() { |  | ||||||
|     var tunnelSession; |  | ||||||
|     if (conf.ddns.tunnel) { |  | ||||||
|       // In the case of a non-existant token, I'm not sure if we want to throw here and prevent
 |  | ||||||
|       // any tunnel connections, or if we  want to ignore it and fall back to the DNS tokens
 |  | ||||||
|       tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     await utils.iterateAllModules(function (mod, domainList) { |  | ||||||
|       if (mod.type !== 'dns@oauth3.org') { return null; } |  | ||||||
| 
 |  | ||||||
|       return startTunnel(tunnelSession, mod, domainList); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     tunnelActive = true; |  | ||||||
|   } |  | ||||||
|   async function disconnectTunnels() { |  | ||||||
|     tunnelClients.disconnect(); |  | ||||||
|     tunnelActive = false; |  | ||||||
|     await Promise.resolve(); |  | ||||||
|   } |  | ||||||
|   async function checkTunnelTokens() { |  | ||||||
|     var oldTokens = tunnelClients.current(); |  | ||||||
| 
 |  | ||||||
|     var newTokens = await utils.iterateAllModules(function checkTokens(mod, domainList) { |  | ||||||
|       if (mod.type !== 'dns@oauth3.org') { return null; } |  | ||||||
| 
 |  | ||||||
|       var domainStr = domainList.slice().sort().join(','); |  | ||||||
|       // If there is already a token handling exactly the domains this modules
 |  | ||||||
|       // needs handled remove it from the list of tokens to be removed. Otherwise
 |  | ||||||
|       // return the module and domain list so we can get new tokens.
 |  | ||||||
|       if (oldTokens[domainStr]) { |  | ||||||
|         delete oldTokens[domainStr]; |  | ||||||
|       } else { |  | ||||||
|         return Promise.resolve({ mod, domainList }); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     await Promise.all(Object.values(oldTokens).map(tunnelClients.remove)); |  | ||||||
| 
 |  | ||||||
|     if (!newTokens.length) { return; } |  | ||||||
| 
 |  | ||||||
|     var tunnelSession; |  | ||||||
|     if (conf.ddns.tunnel) { |  | ||||||
|       // In the case of a non-existant token, I'm not sure if we want to throw here and prevent
 |  | ||||||
|       // any tunnel connections, or if we  want to ignore it and fall back to the DNS tokens
 |  | ||||||
|       tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     await Promise.all(newTokens.map(function ({mod, domainList}) { |  | ||||||
|       return startTunnel(tunnelSession, mod, domainList); |  | ||||||
|     })); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var localAddr, gateway; |  | ||||||
|   async function checkNetworkEnv() { |  | ||||||
|     // Since we can't detect the OS level events when a user plugs in an ethernet cable to recheck
 |  | ||||||
|     // what network environment we are in we check our local network address and the gateway to
 |  | ||||||
|     // determine if we need to run the loopback check and router configuration again.
 |  | ||||||
|     var addr = await network.getPrivateIpAsync(); |  | ||||||
|     // Until the author of the `network` package publishes the pull request we gave him
 |  | ||||||
|     // checking the gateway on our units fails because we have the busybox versions of
 |  | ||||||
|     // the linux commands. Gateway is realistically less important than address, so if
 |  | ||||||
|     // we fail in getting it go ahead and use the null value.
 |  | ||||||
|     var gw; |  | ||||||
|     try { |  | ||||||
|       gw = await network.getGatewayIpAsync(); |  | ||||||
|     } catch (err) { |  | ||||||
|       gw = null; |  | ||||||
|     } |  | ||||||
|     if (localAddr === addr && gateway === gw) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var loopResult = await loopback(loopbackDomain); |  | ||||||
|     var notLooped = Object.keys(loopResult.ports).filter(function (port) { |  | ||||||
|       return !loopResult.ports[port]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // if (notLooped.length) {
 |  | ||||||
|     //   // TODO: try to automatically configure router to forward ports to us.
 |  | ||||||
|     // }
 |  | ||||||
| 
 |  | ||||||
|     // If we are on a public address or all ports we are listening on are forwarded to us then
 |  | ||||||
|     // we don't need the tunnel and we can set the DNS records for all our domains to our public
 |  | ||||||
|     // address. Otherwise we need to use the tunnel to accept traffic. Also since the tunnel will
 |  | ||||||
|     // only be listening on ports 80 and 443 if those are forwarded to us we don't want the tunnel.
 |  | ||||||
|     if (!notLooped.length || (loopResult.ports['80'] && loopResult.ports['443'])) { |  | ||||||
|       if (tunnelActive) { |  | ||||||
|         await disconnectTunnels(); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       if (!tunnelActive) { |  | ||||||
|         await connectAllTunnels(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Don't assign these until the end of the function. This means that if something failed
 |  | ||||||
|     // in the loopback or tunnel connection that we will try to go through the whole process
 |  | ||||||
|     // again next time and hopefully the error is temporary (but if not I'm not sure what the
 |  | ||||||
|     // correct course of action would be anyway).
 |  | ||||||
|     localAddr = addr; |  | ||||||
|     gateway = gw; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var publicAddress; |  | ||||||
|   async function recheckPubAddr() { |  | ||||||
|     await checkNetworkEnv(); |  | ||||||
|     if (tunnelActive) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var addr = await loopback.checkPublicAddr(loopbackDomain); |  | ||||||
|     if (publicAddress === addr) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (conf.debug) { |  | ||||||
|       console.log('previous public address',publicAddress, 'does not match current public address', addr); |  | ||||||
|     } |  | ||||||
|     publicAddress = addr; |  | ||||||
| 
 |  | ||||||
|     await utils.iterateAllModules(function setModuleDNS(mod, domainList) { |  | ||||||
|       if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; } |  | ||||||
| 
 |  | ||||||
|       return utils.getSession(mod.tokenId).then(function (session) { |  | ||||||
|         return dnsCtrl.setDeviceAddress(session, addr, domainList); |  | ||||||
|       }).catch(function (err) { |  | ||||||
|         console.log('error setting DNS records for', domainList.join(', ')); |  | ||||||
|         console.log(err); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getModuleDiffs(prevConf) { |  | ||||||
|     var prevMods = {}; |  | ||||||
|     var curMods = {}; |  | ||||||
| 
 |  | ||||||
|     // this returns a Promise, but since the functions we use are synchronous
 |  | ||||||
|     // and change our enclosed variables we don't need to wait for the return.
 |  | ||||||
|     utils.iterateAllModules(function (mod, domainList) { |  | ||||||
|       if (mod.type !== 'dns@oauth3.org') { return; } |  | ||||||
| 
 |  | ||||||
|       prevMods[mod.id] = { mod, domainList }; |  | ||||||
|       return true; |  | ||||||
|     }, prevConf); |  | ||||||
|     utils.iterateAllModules(function (mod, domainList) { |  | ||||||
|       if (mod.type !== 'dns@oauth3.org') { return; } |  | ||||||
| 
 |  | ||||||
|       curMods[mod.id] = { mod, domainList }; |  | ||||||
|       return true; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Filter out all of the modules that are exactly the same including domainList
 |  | ||||||
|     // since there is no required action to transition.
 |  | ||||||
|     Object.keys(prevMods).map(function (id) { |  | ||||||
|       if (equal(prevMods[id], curMods[id])) { |  | ||||||
|         delete prevMods[id]; |  | ||||||
|         delete curMods[id]; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return {prevMods, curMods}; |  | ||||||
|   } |  | ||||||
|   async function cleanOldDns(prevConf) { |  | ||||||
|     var {prevMods, curMods} = getModuleDiffs(prevConf); |  | ||||||
| 
 |  | ||||||
|     // Then remove DNS records for the domains that we are no longer responsible for.
 |  | ||||||
|     await Promise.all(Object.values(prevMods).map(function ({mod, domainList}) { |  | ||||||
|       // If the module was disabled before there should be any records that we need to clean up
 |  | ||||||
|       if (mod.disabled) { return; } |  | ||||||
| 
 |  | ||||||
|       var oldDomains; |  | ||||||
|       if (!curMods[mod.id] || curMods[mod.id].disabled || mod.tokenId !== curMods[mod.id].mod.tokenId) { |  | ||||||
|         oldDomains = domainList.slice(); |  | ||||||
|       } else { |  | ||||||
|         oldDomains = domainList.filter(function (domain) { |  | ||||||
|           return curMods[mod.id].domainList.indexOf(domain) < 0; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       if (conf.debug) { |  | ||||||
|         console.log('removing old domains for module', mod.id, oldDomains.join(', ')); |  | ||||||
|       } |  | ||||||
|       if (!oldDomains.length) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return utils.getSession(mod.tokenId).then(function (session) { |  | ||||||
|         return dnsCtrl.removeDomains(session, oldDomains); |  | ||||||
|       }); |  | ||||||
|     }).filter(Boolean)); |  | ||||||
|   } |  | ||||||
|   async function setNewDns(prevConf) { |  | ||||||
|     var {prevMods, curMods} = getModuleDiffs(prevConf); |  | ||||||
| 
 |  | ||||||
|     // And add DNS records for any newly added domains.
 |  | ||||||
|     await Promise.all(Object.values(curMods).map(function ({mod, domainList}) { |  | ||||||
|       // Don't set any new records if the module has been disabled.
 |  | ||||||
|       if (mod.disabled) { return; } |  | ||||||
| 
 |  | ||||||
|       var newDomains; |  | ||||||
|       if (!prevMods[mod.id] || mod.tokenId !== prevMods[mod.id].mod.tokenId) { |  | ||||||
|         newDomains = domainList.slice(); |  | ||||||
|       } else { |  | ||||||
|         newDomains = domainList.filter(function (domain) { |  | ||||||
|           return prevMods[mod.id].domainList.indexOf(domain) < 0; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       if (conf.debug) { |  | ||||||
|         console.log('adding new domains for module', mod.id, newDomains.join(', ')); |  | ||||||
|       } |  | ||||||
|       if (!newDomains.length) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return utils.getSession(mod.tokenId).then(function (session) { |  | ||||||
|         return dnsCtrl.setDeviceAddress(session, publicAddress, newDomains); |  | ||||||
|       }); |  | ||||||
|     }).filter(Boolean)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function check() { |  | ||||||
|     recheckPubAddr().catch(function (err) { |  | ||||||
|       console.error('failed to handle all actions needed for DDNS'); |  | ||||||
|       console.error(err); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   check(); |  | ||||||
|   setInterval(check, 5*60*1000); |  | ||||||
| 
 |  | ||||||
|   var curConf; |  | ||||||
|   function updateConf() { |  | ||||||
|     if (curConf && equal(curConf.ddns, conf.ddns) && equal(curConf.domains, conf.domains)) { |  | ||||||
|       // We could update curConf, but since everything we care about is the same...
 |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!curConf || !equal(curConf.ddns.loopback, conf.ddns.loopback)) { |  | ||||||
|       loopbackDomain = 'oauth3.org'; |  | ||||||
|       if (conf.ddns && conf.ddns.loopback) { |  | ||||||
|         if (conf.ddns.loopback.type === 'tunnel@oauth3.org' && conf.ddns.loopback.domain) { |  | ||||||
|           loopbackDomain = conf.ddns.loopback.domain; |  | ||||||
|         } else { |  | ||||||
|           console.error('invalid loopback configuration: bad type or missing domain'); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!curConf) { |  | ||||||
|       // We need to make a deep copy of the config so we can use it next time to
 |  | ||||||
|       // compare and see what setup/cleanup is needed to adapt to the changes.
 |  | ||||||
|       curConf = JSON.parse(JSON.stringify(conf)); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     cleanOldDns(curConf).then(function () { |  | ||||||
|       if (!tunnelActive) { |  | ||||||
|         return setNewDns(curConf); |  | ||||||
|       } |  | ||||||
|       if (equal(curConf.ddns.tunnel, conf.ddns.tunnel)) { |  | ||||||
|         return checkTunnelTokens(); |  | ||||||
|       } else { |  | ||||||
|         return disconnectTunnels().then(connectAllTunnels); |  | ||||||
|       } |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       console.error('error transitioning DNS between configurations'); |  | ||||||
|       console.error(err); |  | ||||||
|     }).then(function () { |  | ||||||
|       // We need to make a deep copy of the config so we can use it next time to
 |  | ||||||
|       // compare and see what setup/cleanup is needed to adapt to the changes.
 |  | ||||||
|       curConf = JSON.parse(JSON.stringify(conf)); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   updateConf(); |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     loopbackServer:     loopback.server |  | ||||||
|   , setDeviceAddress:   dnsCtrl.setDeviceAddress |  | ||||||
|   , getDeviceAddresses: dnsCtrl.getDeviceAddresses |  | ||||||
|   , recheckPubAddr:     recheckPubAddr |  | ||||||
|   , updateConf:         updateConf |  | ||||||
|   , challenge |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,116 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, conf) { |  | ||||||
|   var pending = {}; |  | ||||||
| 
 |  | ||||||
|   async function _checkPublicAddr(host) { |  | ||||||
|     var result = await deps.request({ |  | ||||||
|       method: 'GET' |  | ||||||
|     , url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/checkip' |  | ||||||
|     , json: true |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     if (!result.body) { |  | ||||||
|       throw new Error('No response body in request for public address'); |  | ||||||
|     } |  | ||||||
|     if (result.body.error) { |  | ||||||
|       // Note that the error on the body will probably have a message that overwrites the default
 |  | ||||||
|       throw Object.assign(new Error('error in check IP response'), result.body.error); |  | ||||||
|     } |  | ||||||
|     if (!result.body.address) { |  | ||||||
|       throw new Error("public address resonse doesn't contain address: "+JSON.stringify(result.body)); |  | ||||||
|     } |  | ||||||
|     return result.body.address; |  | ||||||
|   } |  | ||||||
|   async function checkPublicAddr(provider) { |  | ||||||
|     var directives = await deps.OAUTH3.discover(provider); |  | ||||||
|     return _checkPublicAddr(directives.api); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function checkSinglePort(host, address, port) { |  | ||||||
|     var crypto = require('crypto'); |  | ||||||
|     var token   = crypto.randomBytes(8).toString('hex'); |  | ||||||
|     var keyAuth = crypto.randomBytes(32).toString('hex'); |  | ||||||
|     pending[token] = keyAuth; |  | ||||||
| 
 |  | ||||||
|     var reqObj = { |  | ||||||
|       method: 'POST' |  | ||||||
|     , url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/loopback' |  | ||||||
|     , timeout: 20*1000 |  | ||||||
|     , json: { |  | ||||||
|         address: address |  | ||||||
|       , port: port |  | ||||||
|       , token: token |  | ||||||
|       , keyAuthorization: keyAuth |  | ||||||
|       , iat: Date.now() |  | ||||||
|       , timeout: 18*1000 |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     var result; |  | ||||||
|     try { |  | ||||||
|       result = await deps.request(reqObj); |  | ||||||
|     } catch (err) { |  | ||||||
|       delete pending[token]; |  | ||||||
|       if (conf.debug) { |  | ||||||
|         console.log('error making loopback request for port ' + port + ' loopback', err.message); |  | ||||||
|       } |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     delete pending[token]; |  | ||||||
|     if (!result.body) { |  | ||||||
|       if (conf.debug) { |  | ||||||
|         console.log('No response body in loopback request for port '+port); |  | ||||||
|       } |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     // If the loopback requests don't go to us then there are all kinds of ways it could
 |  | ||||||
|     // error, but none of them really provide much extra information so we don't do
 |  | ||||||
|     // anything that will break the PromiseA.all out and mask the other results.
 |  | ||||||
|     if (conf.debug && result.body.error) { |  | ||||||
|       console.log('error on remote side of port '+port+' loopback', result.body.error); |  | ||||||
|     } |  | ||||||
|     return !!result.body.success; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function loopback(provider) { |  | ||||||
|     var directives = await deps.OAUTH3.discover(provider); |  | ||||||
|     var address = await _checkPublicAddr(directives.api); |  | ||||||
|     if (conf.debug) { |  | ||||||
|       console.log('checking to see if', address, 'gets back to us'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var ports = require('../servers').listeners.tcp.list(); |  | ||||||
|     var values = await deps.PromiseA.all(ports.map(function (port) { |  | ||||||
|       return checkSinglePort(directives.api, address, port); |  | ||||||
|     })); |  | ||||||
| 
 |  | ||||||
|     if (conf.debug && Object.keys(pending).length) { |  | ||||||
|       console.log('remaining loopback tokens', pending); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|       address: address |  | ||||||
|     , ports: ports.reduce(function (obj, port, ind) { |  | ||||||
|         obj[port] = values[ind]; |  | ||||||
|         return obj; |  | ||||||
|       }, {}) |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   loopback.checkPublicAddr = checkPublicAddr; |  | ||||||
|   loopback.server = require('http').createServer(function (req, res) { |  | ||||||
|     var parsed = require('url').parse(req.url); |  | ||||||
|     var token = parsed.pathname.replace('/.well-known/cloud-challenge/', ''); |  | ||||||
|     if (pending[token]) { |  | ||||||
|       res.setHeader('Content-Type', 'text/plain'); |  | ||||||
|       res.end(pending[token]); |  | ||||||
|     } else { |  | ||||||
|       res.statusCode = 404; |  | ||||||
|       res.end(); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   return loopback; |  | ||||||
| }; |  | ||||||
| @ -1,191 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config) { |  | ||||||
|   var stunnel = require('stunnel'); |  | ||||||
|   var jwt = require('jsonwebtoken'); |  | ||||||
|   var activeTunnels = {}; |  | ||||||
|   var activeDomains = {}; |  | ||||||
| 
 |  | ||||||
|   var customNet = { |  | ||||||
|     createConnection: function (opts, cb) { |  | ||||||
|       console.log('[gl.tunnel] creating connection'); |  | ||||||
| 
 |  | ||||||
|       // here "reader" means the socket that looks like the connection being accepted
 |  | ||||||
|       // here "writer" means the remote-looking part of the socket that driving the connection
 |  | ||||||
|       var writer; |  | ||||||
| 
 |  | ||||||
|       function usePair(err, reader) { |  | ||||||
|         if (err) { |  | ||||||
|           process.nextTick(function () { |  | ||||||
|             writer.emit('error', err); |  | ||||||
|           }); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         var wrapOpts = Object.assign({localAddress: '127.0.0.2', localPort: 'tunnel-0'}, opts); |  | ||||||
|         wrapOpts.firstChunk = opts.data; |  | ||||||
|         wrapOpts.hyperPeek = !!opts.data; |  | ||||||
| 
 |  | ||||||
|         // Also override the remote and local address info. We use `defineProperty` because
 |  | ||||||
|         // otherwise we run into problems of setting properties with only getters defined.
 |  | ||||||
|         Object.defineProperty(reader, 'remoteAddress', { value: wrapOpts.remoteAddress }); |  | ||||||
|         Object.defineProperty(reader, 'remotePort',    { value: wrapOpts.remotePort }); |  | ||||||
|         Object.defineProperty(reader, 'remoteFamiliy', { value: wrapOpts.remoteFamiliy }); |  | ||||||
|         Object.defineProperty(reader, 'localAddress',  { value: wrapOpts.localAddress }); |  | ||||||
|         Object.defineProperty(reader, 'localPort',     { value: wrapOpts.localPort }); |  | ||||||
|         Object.defineProperty(reader, 'localFamiliy',  { value: wrapOpts.localFamiliy }); |  | ||||||
| 
 |  | ||||||
|         deps.tcp.handler(reader, wrapOpts); |  | ||||||
|         process.nextTick(function () { |  | ||||||
|           // this cb will cause the stream to emit its (actually) first data event
 |  | ||||||
|           // (even though it already gave a peek into that first data chunk)
 |  | ||||||
|           console.log('[tunnel] callback, data should begin to flow'); |  | ||||||
|           cb(); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // We used to use `stream-pair` for non-tls connections, but there are places
 |  | ||||||
|       // that require properties/functions to be present on the socket that aren't
 |  | ||||||
|       // present on a JSStream so it caused problems.
 |  | ||||||
|       writer = require('socket-pair').create(usePair); |  | ||||||
|       return writer; |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   function fillData(data) { |  | ||||||
|     if (typeof data === 'string') { |  | ||||||
|       data = { jwt: data }; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!data.jwt) { |  | ||||||
|       throw new Error("missing 'jwt' from tunnel data"); |  | ||||||
|     } |  | ||||||
|     var decoded = jwt.decode(data.jwt); |  | ||||||
|     if (!decoded) { |  | ||||||
|       throw new Error('invalid JWT'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!data.tunnelUrl) { |  | ||||||
|       if (!decoded.aud) { |  | ||||||
|         throw new Error('missing tunnelUrl and audience'); |  | ||||||
|       } |  | ||||||
|       data.tunnelUrl = 'wss://' + decoded.aud + '/'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     data.domains = (decoded.domains || []).slice().sort().join(','); |  | ||||||
|     if (!data.domains) { |  | ||||||
|       throw new Error('JWT contains no domains to be forwarded'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return data; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function removeToken(data) { |  | ||||||
|     data = fillData(data); |  | ||||||
| 
 |  | ||||||
|     // Not sure if we might want to throw an error indicating the token didn't
 |  | ||||||
|     // even belong to a  server that existed, but since it never existed we can
 |  | ||||||
|     // consider it as "removed".
 |  | ||||||
|     if (!activeTunnels[data.tunnelUrl]) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.log('removing token from tunnel at', data.tunnelUrl); |  | ||||||
|     return activeTunnels[data.tunnelUrl].clear(data.jwt).then(function () { |  | ||||||
|       delete activeDomains[data.domains]; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function addToken(data) { |  | ||||||
|     data = fillData(data); |  | ||||||
| 
 |  | ||||||
|     if (activeDomains[data.domains]) { |  | ||||||
|       // If already have a token with the exact same domains and to the same tunnel
 |  | ||||||
|       // server there isn't really a need to add a new one
 |  | ||||||
|       if (activeDomains[data.domains].tunnelUrl === data.tunnelUrl) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       // Otherwise we want to detach from the other tunnel server in favor of the new one
 |  | ||||||
|       console.warn('added token with the exact same domains as another'); |  | ||||||
|       await removeToken(activeDomains[data.domains]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!activeTunnels[data.tunnelUrl]) { |  | ||||||
|       console.log('creating new tunnel client for', data.tunnelUrl); |  | ||||||
|       // We create the tunnel without an initial token so we can append the token and
 |  | ||||||
|       // get the promise that should tell us more about if it worked or not.
 |  | ||||||
|       activeTunnels[data.tunnelUrl] = stunnel.connect({ |  | ||||||
|         stunneld: data.tunnelUrl |  | ||||||
|       , net: customNet |  | ||||||
|         // NOTE: the ports here aren't that important since we are providing a custom
 |  | ||||||
|         // `net.createConnection` that doesn't actually use the port. What is important
 |  | ||||||
|         // is that any services we are interested in are listed in this object and have
 |  | ||||||
|         // a '*' sub-property.
 |  | ||||||
|       , services: { |  | ||||||
|           https: { '*': 443 } |  | ||||||
|         , http:  { '*': 80 } |  | ||||||
|         , smtp:  { '*': 25 } |  | ||||||
|         , smtps: { '*': 587 /*also 465/starttls*/ } |  | ||||||
|         , ssh:   { '*': 22 } |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.log('appending token to tunnel at', data.tunnelUrl, 'for domains', data.domains); |  | ||||||
|     await activeTunnels[data.tunnelUrl].append(data.jwt); |  | ||||||
| 
 |  | ||||||
|     // Now that we know the tunnel server accepted our token we can save it
 |  | ||||||
|     // to keep record of what domains we are handling and what tunnel server
 |  | ||||||
|     // those domains should go to.
 |  | ||||||
|     activeDomains[data.domains] = data; |  | ||||||
| 
 |  | ||||||
|     // This is mostly for the start, but return the host for the tunnel server
 |  | ||||||
|     // we've connected to (after stripping the protocol and path away).
 |  | ||||||
|     return data.tunnelUrl.replace(/^[a-z]*:\/\//i, '').replace(/\/.*/, ''); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function acquireToken(session, domains) { |  | ||||||
|     var OAUTH3 = deps.OAUTH3; |  | ||||||
| 
 |  | ||||||
|     // The OAUTH3 library stores some things on the root session object that we usually
 |  | ||||||
|     // just leave inside the token, but we need to pull those out before we use it here
 |  | ||||||
|     session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss; |  | ||||||
|     session.client_uri = session.client_uri || session.token.azp; |  | ||||||
|     session.scope = session.scope || session.token.scp; |  | ||||||
| 
 |  | ||||||
|     console.log('asking for tunnel token from', session.token.aud); |  | ||||||
|     var opts = { |  | ||||||
|       api: 'tunnel.token' |  | ||||||
|     , session: session |  | ||||||
|     , data: { |  | ||||||
|         domains: domains |  | ||||||
|       , device: { |  | ||||||
|           hostname: config.device.hostname |  | ||||||
|         , id: config.device.uid || config.device.id |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     var directives = await OAUTH3.discover(session.token.aud); |  | ||||||
|     var tokenData = await OAUTH3.api(directives.api, opts); |  | ||||||
|     return addToken(tokenData); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function disconnectAll() { |  | ||||||
|     Object.keys(activeTunnels).forEach(function (key) { |  | ||||||
|       activeTunnels[key].end(); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function currentTokens() { |  | ||||||
|     return JSON.parse(JSON.stringify(activeDomains)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     start:       acquireToken |  | ||||||
|   , startDirect: addToken |  | ||||||
|   , remove:      removeToken |  | ||||||
|   , disconnect:  disconnectAll |  | ||||||
|   , current:     currentTokens |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,102 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, conf) { |  | ||||||
| 
 |  | ||||||
|   async function getSession(id) { |  | ||||||
|     var session = await deps.storage.tokens.get(id); |  | ||||||
|     if (!session) { |  | ||||||
|       throw new Error('no user token with ID "' + id + '"'); |  | ||||||
|     } |  | ||||||
|     return session; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function iterateAllModules(action, curConf) { |  | ||||||
|     curConf = curConf || conf; |  | ||||||
|     var promises = []; |  | ||||||
| 
 |  | ||||||
|     curConf.domains.forEach(function (dom) { |  | ||||||
|       if (!dom.modules || !Array.isArray(dom.modules.ddns) || !dom.modules.ddns.length) { |  | ||||||
|         return null; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // For the time being all of our things should only be tried once (regardless if it succeeded)
 |  | ||||||
|       // TODO: revisit this behavior when we support multiple ways of setting records, and/or
 |  | ||||||
|       // if we want to allow later modules to run if early modules fail.
 |  | ||||||
|       promises.push(dom.modules.ddns.reduce(function (prom, mod) { |  | ||||||
|         if (prom) { return prom; } |  | ||||||
|         return action(mod, dom.names); |  | ||||||
|       }, null)); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     curConf.ddns.modules.forEach(function (mod) { |  | ||||||
|       promises.push(action(mod, mod.domains)); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return Promise.all(promises.filter(Boolean)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var tldCache = {}; |  | ||||||
|   async function updateTldCache(provider) { |  | ||||||
|     var reqObj = { |  | ||||||
|       url: deps.OAUTH3.url.normalize(provider) + '/api/com.daplie.domains/prices' |  | ||||||
|     , method: 'GET' |  | ||||||
|     , json: true |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     var resp = await deps.OAUTH3.request(reqObj); |  | ||||||
|     var tldObj = {}; |  | ||||||
|     resp.data.forEach(function (tldInfo) { |  | ||||||
|       if (tldInfo.enabled) { |  | ||||||
|         tldObj[tldInfo.tld] = true; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     tldCache[provider] = { |  | ||||||
|       time: Date.now() |  | ||||||
|     , tlds: tldObj |  | ||||||
|     }; |  | ||||||
|     return tldObj; |  | ||||||
|   } |  | ||||||
|   async function getTlds(provider) { |  | ||||||
|     // If we've never cached the results we need to return the promise that will fetch the result,
 |  | ||||||
|     // otherwise we can return the cached value. If the cached value has "expired", we can still
 |  | ||||||
|     // return the cached value we just want to update the cache in parellel (making sure we only
 |  | ||||||
|     // update once).
 |  | ||||||
|     if (!tldCache[provider]) { |  | ||||||
|       tldCache[provider] = { |  | ||||||
|         updating: true |  | ||||||
|       , tlds: updateTldCache(provider) |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|     if (!tldCache[provider].updating && Date.now() - tldCache[provider].time > 24 * 60 * 60 * 1000) { |  | ||||||
|       tldCache[provider].updating = true; |  | ||||||
|       updateTldCache(provider); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return tldCache[provider].tlds; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function splitDomains(provider, domains) { |  | ||||||
|     var tlds = await getTlds(provider); |  | ||||||
|     return domains.map(function (domain) { |  | ||||||
|       var split = domain.split('.'); |  | ||||||
|       var tldSegCnt = tlds[split.slice(-2).join('.')] ? 2 : 1; |  | ||||||
| 
 |  | ||||||
|       // Currently assuming that the sld can't contain dots, and that the tld can have at
 |  | ||||||
|       // most one dot. Not 100% sure this is a valid assumption, but exceptions should be
 |  | ||||||
|       // rare even if the assumption isn't valid.
 |  | ||||||
|       return { |  | ||||||
|         tld: split.slice(-tldSegCnt).join('.') |  | ||||||
|       , sld: split.slice(-tldSegCnt - 1, -tldSegCnt).join('.') |  | ||||||
|       , sub: split.slice(0, -tldSegCnt - 1).join('.') |  | ||||||
|       }; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     getSession |  | ||||||
|   , iterateAllModules |  | ||||||
|   , getTlds |  | ||||||
|   , splitDomains |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,30 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.match = function (pattern, domainname) { |  | ||||||
|   // Everything matches '*'
 |  | ||||||
|   if (pattern === '*') { |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (/^\*./.test(pattern)) { |  | ||||||
|     // get rid of the leading "*." to more easily check the servername against it
 |  | ||||||
|     pattern = pattern.slice(2); |  | ||||||
|     return pattern === domainname.slice(-pattern.length); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // pattern doesn't contains any wildcards, so exact match is required
 |  | ||||||
|   return pattern === domainname; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| module.exports.separatePort = function (fullHost) { |  | ||||||
|   var match = /^(.*?)(:\d+)?$/.exec(fullHost); |  | ||||||
| 
 |  | ||||||
|   if (match[2]) { |  | ||||||
|     match[2] = match[2].replace(':', ''); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     host: match[1] |  | ||||||
|   , port: match[2] |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										117
									
								
								lib/match-ips.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								lib/match-ips.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var PromiseA = require('bluebird'); | ||||||
|  | 
 | ||||||
|  | module.exports.match = function (servername, opts) { | ||||||
|  |   return PromiseA.promisify(require('ipify'))().then(function (externalIp) { | ||||||
|  |     var dns = PromiseA.promisifyAll(require('dns')); | ||||||
|  | 
 | ||||||
|  |     opts.externalIps = [ { address: externalIp, family: 'IPv4' } ]; | ||||||
|  |     opts.ifaces = require('./local-ip.js').find({ externals: opts.externalIps }); | ||||||
|  |     opts.externalIfaces = Object.keys(opts.ifaces).reduce(function (all, iname) { | ||||||
|  |       var iface = opts.ifaces[iname]; | ||||||
|  | 
 | ||||||
|  |       iface.ipv4.forEach(function (addr) { | ||||||
|  |         if (addr.external) { | ||||||
|  |           addr.iface = iname; | ||||||
|  |           all.push(addr); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       iface.ipv6.forEach(function (addr) { | ||||||
|  |         if (addr.external) { | ||||||
|  |           addr.iface = iname; | ||||||
|  |           all.push(addr); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       return all; | ||||||
|  |     }, []).filter(Boolean); | ||||||
|  | 
 | ||||||
|  |     function resolveIps(hostname) { | ||||||
|  |       var allIps = []; | ||||||
|  | 
 | ||||||
|  |       return PromiseA.all([ | ||||||
|  |         dns.resolve4Async(hostname).then(function (records) { | ||||||
|  |             records.forEach(function (ip) { | ||||||
|  |               allIps.push({ | ||||||
|  |                 address: ip | ||||||
|  |               , family: 'IPv4' | ||||||
|  |               }); | ||||||
|  |             }); | ||||||
|  |           }, function () {}) | ||||||
|  |         , dns.resolve6Async(hostname).then(function (records) { | ||||||
|  |             records.forEach(function (ip) { | ||||||
|  |               allIps.push({ | ||||||
|  |                 address: ip | ||||||
|  |               , family: 'IPv6' | ||||||
|  |               }); | ||||||
|  |             }); | ||||||
|  |           }, function () {}) | ||||||
|  |       ]).then(function () { | ||||||
|  |         return allIps; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function resolveIpsAndCnames(hostname) { | ||||||
|  |       return PromiseA.all([ | ||||||
|  |         resolveIps(hostname) | ||||||
|  |       , dns.resolveCnameAsync(hostname).then(function (records) { | ||||||
|  |           return PromiseA.all(records.map(function (hostname) { | ||||||
|  |             return resolveIps(hostname); | ||||||
|  |           })).then(function (allIps) { | ||||||
|  |             return allIps.reduce(function (all, ips) { | ||||||
|  |               return all.concat(ips); | ||||||
|  |             }, []); | ||||||
|  |           }); | ||||||
|  |         }, function () { | ||||||
|  |           return []; | ||||||
|  |         }) | ||||||
|  |       ]).then(function (ips) { | ||||||
|  |         return ips.reduce(function (all, set) { | ||||||
|  |           return all.concat(set); | ||||||
|  |         }, []); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return resolveIpsAndCnames(servername).then(function (allIps) { | ||||||
|  |       var matchingIps = []; | ||||||
|  | 
 | ||||||
|  |       if (!allIps.length) { | ||||||
|  |         console.warn("Could not resolve '" + servername + "'"); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // { address, family }
 | ||||||
|  |       allIps.some(function (ip) { | ||||||
|  |         function match(addr) { | ||||||
|  |           if (ip.address === addr.address) { | ||||||
|  |             matchingIps.push(addr); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         opts.externalIps.forEach(match); | ||||||
|  |         // opts.externalIfaces.forEach(match);
 | ||||||
|  | 
 | ||||||
|  |         Object.keys(opts.ifaces).forEach(function (iname) { | ||||||
|  |           var iface = opts.ifaces[iname]; | ||||||
|  | 
 | ||||||
|  |           iface.ipv4.forEach(match); | ||||||
|  |           iface.ipv6.forEach(match); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return matchingIps.length; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       matchingIps.externalIps = { | ||||||
|  |         ipv4: [ | ||||||
|  |           { address: externalIp | ||||||
|  |           , family: 'IPv4' | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       , ipv6: [ | ||||||
|  |         ] | ||||||
|  |       }; | ||||||
|  |       matchingIps.matchingIps = matchingIps; | ||||||
|  |       return matchingIps; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										203
									
								
								lib/mdns.js
									
									
									
									
									
								
							
							
						
						
									
										203
									
								
								lib/mdns.js
									
									
									
									
									
								
							| @ -1,203 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var PromiseA = require('bluebird'); |  | ||||||
| var queryName = '_cloud._tcp.local'; |  | ||||||
| var dnsSuite = require('dns-suite'); |  | ||||||
| 
 |  | ||||||
| function createResponse(name, ownerIds, packet, ttl, mainPort) { |  | ||||||
|   var rpacket = { |  | ||||||
|     header: { |  | ||||||
|       id: packet.header.id |  | ||||||
|     , qr: 1 |  | ||||||
|     , opcode: 0 |  | ||||||
|     , aa: 1 |  | ||||||
|     , tc: 0 |  | ||||||
|     , rd: 0 |  | ||||||
|     , ra: 0 |  | ||||||
|     , res1:  0 |  | ||||||
|     , res2:  0 |  | ||||||
|     , res3:  0 |  | ||||||
|     , rcode: 0 |  | ||||||
|   , } |  | ||||||
|   , question: packet.question |  | ||||||
|   , answer: [] |  | ||||||
|   , authority: [] |  | ||||||
|   , additional: [] |  | ||||||
|   , edns_options: [] |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   rpacket.answer.push({ |  | ||||||
|     name: queryName |  | ||||||
|   , typeName: 'PTR' |  | ||||||
|   , ttl: ttl |  | ||||||
|   , className: 'IN' |  | ||||||
|   , data: name + '.' + queryName |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   var ifaces = require('./local-ip').find(); |  | ||||||
|   Object.keys(ifaces).forEach(function (iname) { |  | ||||||
|     var iface = ifaces[iname]; |  | ||||||
| 
 |  | ||||||
|     iface.ipv4.forEach(function (addr) { |  | ||||||
|       rpacket.additional.push({ |  | ||||||
|         name: name + '.local' |  | ||||||
|       , typeName: 'A' |  | ||||||
|       , ttl: ttl |  | ||||||
|       , className: 'IN' |  | ||||||
|       , address: addr.address |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     iface.ipv6.forEach(function (addr) { |  | ||||||
|       rpacket.additional.push({ |  | ||||||
|         name: name + '.local' |  | ||||||
|       , typeName: 'AAAA' |  | ||||||
|       , ttl: ttl |  | ||||||
|       , className: 'IN' |  | ||||||
|       , address: addr.address |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   rpacket.additional.push({ |  | ||||||
|     name: name + '.' + queryName |  | ||||||
|   , typeName: 'SRV' |  | ||||||
|   , ttl: ttl |  | ||||||
|   , className: 'IN' |  | ||||||
|   , priority: 1 |  | ||||||
|   , weight: 0 |  | ||||||
|   , port: mainPort |  | ||||||
|   , target: name + ".local" |  | ||||||
|   }); |  | ||||||
|   rpacket.additional.push({ |  | ||||||
|     name: name + '._device-info.' + queryName |  | ||||||
|   , typeName: 'TXT' |  | ||||||
|   , ttl: ttl |  | ||||||
|   , className: 'IN' |  | ||||||
|   , data: ["model=CloudHome1,1", "dappsvers=1"] |  | ||||||
|   }); |  | ||||||
|   ownerIds.forEach(function (id) { |  | ||||||
|     rpacket.additional.push({ |  | ||||||
|       name: name + '._owner-id.' + queryName |  | ||||||
|     , typeName: 'TXT' |  | ||||||
|     , ttl: ttl |  | ||||||
|     , className: 'IN' |  | ||||||
|     , data: [id] |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   return dnsSuite.DNSPacket.write(rpacket); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config) { |  | ||||||
|   var socket; |  | ||||||
|   var nextBroadcast = -1; |  | ||||||
| 
 |  | ||||||
|   function handlePacket(message, rinfo) { |  | ||||||
|     // console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port);
 |  | ||||||
| 
 |  | ||||||
|     var packet; |  | ||||||
|     try { |  | ||||||
|       packet = dnsSuite.DNSPacket.parse(message); |  | ||||||
|     } |  | ||||||
|     catch (er) { |  | ||||||
|       // `dns-suite` actually errors on a lot of the packets floating around in our network,
 |  | ||||||
|       // so don't bother logging any errors. (We still use `dns-suite` because unlike `dns-js`
 |  | ||||||
|       // it can successfully craft the one packet we want to send.)
 |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Only respond to queries.
 |  | ||||||
|     if (packet.header.qr !== 0) {  return; } |  | ||||||
|     // Only respond if they were asking for cloud devices.
 |  | ||||||
|     if (packet.question.length !== 1)           { return; } |  | ||||||
|     if (packet.question[0].name !== queryName)  { return; } |  | ||||||
|     if (packet.question[0].typeName !== 'PTR')  { return; } |  | ||||||
|     if (packet.question[0].className !== 'IN' ) { return; } |  | ||||||
| 
 |  | ||||||
|     var proms = [ |  | ||||||
|       deps.storage.mdnsId.get() |  | ||||||
|     , deps.storage.owners.all().then(function (owners) { |  | ||||||
|         // The ID is the sha256 hash of the PPID, which shouldn't be reversible and therefore
 |  | ||||||
|         // should be safe to expose without needing authentication.
 |  | ||||||
|         return owners.map(function (owner) { |  | ||||||
|           return owner.id; |  | ||||||
|         }); |  | ||||||
|       }) |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     PromiseA.all(proms).then(function (results) { |  | ||||||
|       var resp = createResponse(results[0], results[1], packet, config.mdns.ttl, deps.tcp.mainPort); |  | ||||||
|       var now = Date.now(); |  | ||||||
|       if (now > nextBroadcast) { |  | ||||||
|         socket.send(resp, config.mdns.port, config.mdns.broadcast); |  | ||||||
|         nextBroadcast = now + config.mdns.ttl * 1000; |  | ||||||
|       } else { |  | ||||||
|         socket.send(resp, rinfo.port, rinfo.address); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function start() { |  | ||||||
|     socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true }); |  | ||||||
|     socket.on('message', handlePacket); |  | ||||||
| 
 |  | ||||||
|     return new Promise(function (resolve, reject) { |  | ||||||
|       socket.once('error', reject); |  | ||||||
| 
 |  | ||||||
|       socket.bind(config.mdns.port, function () { |  | ||||||
|         var addr = this.address(); |  | ||||||
|         console.log('bound on UDP %s:%d for mDNS', addr.address, addr.port); |  | ||||||
| 
 |  | ||||||
|         socket.setBroadcast(true); |  | ||||||
|         socket.addMembership(config.mdns.broadcast); |  | ||||||
|         // This is supposed to be a local device discovery mechanism, so we shouldn't
 |  | ||||||
|         // need to hop through any gateways. This helps with security by making it
 |  | ||||||
|         // much more difficult for someone to use us as part of a DDoS attack by
 |  | ||||||
|         // spoofing the UDP address a request came from.
 |  | ||||||
|         socket.setTTL(1); |  | ||||||
| 
 |  | ||||||
|         socket.removeListener('error', reject); |  | ||||||
|         resolve(); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   function stop() { |  | ||||||
|     return new Promise(function (resolve, reject) { |  | ||||||
|       socket.once('error', reject); |  | ||||||
| 
 |  | ||||||
|       socket.close(function () { |  | ||||||
|         socket.removeListener('error', reject); |  | ||||||
|         socket = null; |  | ||||||
|         resolve(); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function updateConf() { |  | ||||||
|     var promise; |  | ||||||
|     if (config.mdns.disabled) { |  | ||||||
|       if (socket) { |  | ||||||
|         promise = stop(); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       if (!socket) { |  | ||||||
|         promise = start(); |  | ||||||
|       } else if (socket.address().port !== config.mdns.port) { |  | ||||||
|         promise = stop().then(start); |  | ||||||
|       } else { |  | ||||||
|         // Can't check membership, so just add the current broadcast address to make sure
 |  | ||||||
|         // it's set. If it's already set it will throw an exception (at least on linux).
 |  | ||||||
|         try { |  | ||||||
|           socket.addMembership(config.mdns.broadcast); |  | ||||||
|         } catch (e) {} |  | ||||||
|         promise = Promise.resolve(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   updateConf(); |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     updateConf |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										179
									
								
								lib/servers.js
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								lib/servers.js
									
									
									
									
									
								
							| @ -1,179 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var serversMap = module.exports._serversMap = {}; |  | ||||||
| var dgramMap = module.exports._dgramMap = {}; |  | ||||||
| var PromiseA = require('bluebird'); |  | ||||||
| 
 |  | ||||||
| module.exports.addTcpListener = function (port, handler) { |  | ||||||
|   return new PromiseA(function (resolve, reject) { |  | ||||||
|     var stat = serversMap[port]; |  | ||||||
| 
 |  | ||||||
|     if (stat) { |  | ||||||
|       if (stat._closing) { |  | ||||||
|         stat.server.destroy(); |  | ||||||
|       } else { |  | ||||||
|         // We're already listening on the port, so we only have 2 options. We can either
 |  | ||||||
|         // replace the handler or reject with an error. (Though neither is really needed
 |  | ||||||
|         // if the handlers are the same). Until there is reason to do otherwise we are
 |  | ||||||
|         // opting for the replacement.
 |  | ||||||
|         stat.handler = handler; |  | ||||||
|         resolve(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var enableDestroy = require('server-destroy'); |  | ||||||
|     var net = require('net'); |  | ||||||
|     var resolved; |  | ||||||
|     var server = net.createServer({allowHalfOpen: true}); |  | ||||||
| 
 |  | ||||||
|     stat = serversMap[port] = { |  | ||||||
|       server: server |  | ||||||
|     , handler: handler |  | ||||||
|     , _closing: false |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     // Add .destroy so we can close all open connections. Better if added before listen
 |  | ||||||
|     // to eliminate any possibility of it missing an early connection in it's records.
 |  | ||||||
|     enableDestroy(server); |  | ||||||
| 
 |  | ||||||
|     server.on('connection', function (conn) { |  | ||||||
|       conn.__port = port; |  | ||||||
|       conn.__proto = 'tcp'; |  | ||||||
|       stat.handler(conn); |  | ||||||
|     }); |  | ||||||
|     server.on('close', function () { |  | ||||||
|       console.log('TCP server on port %d closed', port); |  | ||||||
|       delete serversMap[port]; |  | ||||||
|     }); |  | ||||||
|     server.on('error', function (e) { |  | ||||||
|       if (!resolved) { |  | ||||||
|         reject(e); |  | ||||||
|       } else if (handler.onError) { |  | ||||||
|         handler.onError(e); |  | ||||||
|       } else { |  | ||||||
|         throw e; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     server.listen(port, function () { |  | ||||||
|       resolved = true; |  | ||||||
|       resolve(); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| module.exports.closeTcpListener = function (port, timeout) { |  | ||||||
|   return new PromiseA(function (resolve) { |  | ||||||
|     var stat = serversMap[port]; |  | ||||||
|     if (!stat) { |  | ||||||
|       resolve(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     stat._closing = true; |  | ||||||
| 
 |  | ||||||
|     var timeoutId; |  | ||||||
|     if (timeout) { |  | ||||||
|       timeoutId = setTimeout(() => stat.server.destroy(), timeout); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     stat.server.once('close', function () { |  | ||||||
|       clearTimeout(timeoutId); |  | ||||||
|       resolve(); |  | ||||||
|     }); |  | ||||||
|     stat.server.close(); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| module.exports.destroyTcpListener = function (port) { |  | ||||||
|   var stat = serversMap[port]; |  | ||||||
|   if (stat) { |  | ||||||
|     stat.server.destroy(); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| module.exports.listTcpListeners = function () { |  | ||||||
|   return Object.keys(serversMap).map(Number).filter(function (port) { |  | ||||||
|     return port && !serversMap[port]._closing; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| module.exports.addUdpListener = function (port, handler) { |  | ||||||
|   return new PromiseA(function (resolve, reject) { |  | ||||||
|     var stat = dgramMap[port]; |  | ||||||
| 
 |  | ||||||
|     if (stat) { |  | ||||||
|       // we'll replace the current listener
 |  | ||||||
|       stat.handler = handler; |  | ||||||
|       resolve(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var dgram = require('dgram'); |  | ||||||
|     var server = dgram.createSocket({type: 'udp4', reuseAddr: true}); |  | ||||||
|     var resolved = false; |  | ||||||
| 
 |  | ||||||
|     stat = dgramMap[port] = { |  | ||||||
|       server: server |  | ||||||
|     , handler: handler |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     server.on('message', function (msg, rinfo) { |  | ||||||
|       msg._size = rinfo.size; |  | ||||||
|       msg._remoteFamily = rinfo.family; |  | ||||||
|       msg._remoteAddress = rinfo.address; |  | ||||||
|       msg._remotePort = rinfo.port; |  | ||||||
|       msg._port = port; |  | ||||||
|       stat.handler(msg); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     server.on('error', function (err) { |  | ||||||
|       if (!resolved) { |  | ||||||
|         delete dgramMap[port]; |  | ||||||
|         reject(err); |  | ||||||
|       } |  | ||||||
|       else if (stat.handler.onError) { |  | ||||||
|         stat.handler.onError(err); |  | ||||||
|       } |  | ||||||
|       else { |  | ||||||
|         throw err; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     server.on('close', function () { |  | ||||||
|       delete dgramMap[port]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     server.bind(port, function () { |  | ||||||
|       resolved = true; |  | ||||||
|       resolve(); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| module.exports.closeUdpListener = function (port) { |  | ||||||
|   var stat = dgramMap[port]; |  | ||||||
|   if (!stat) { |  | ||||||
|     return PromiseA.resolve(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return new PromiseA(function (resolve) { |  | ||||||
|     stat.server.once('close', resolve); |  | ||||||
|     stat.server.close(); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| module.exports.listUdpListeners = function () { |  | ||||||
|   return Object.keys(dgramMap).map(Number).filter(Boolean); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| module.exports.listeners = { |  | ||||||
|   tcp: { |  | ||||||
|     add: module.exports.addTcpListener |  | ||||||
|   , close: module.exports.closeTcpListener |  | ||||||
|   , destroy: module.exports.destroyTcpListener |  | ||||||
|   , list: module.exports.listTcpListeners |  | ||||||
|   } |  | ||||||
| , udp: { |  | ||||||
|     add: module.exports.addUdpListener |  | ||||||
|   , close: module.exports.closeUdpListener |  | ||||||
|   , list: module.exports.listUdpListeners |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| @ -1,91 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config) { |  | ||||||
|   var PromiseA = require('bluebird'); |  | ||||||
|   var server; |  | ||||||
| 
 |  | ||||||
|   function curState() { |  | ||||||
|     var addr = server && server.address(); |  | ||||||
|     if (!addr) { |  | ||||||
|       return PromiseA.resolve({running: false}); |  | ||||||
|     } |  | ||||||
|     return PromiseA.resolve({ |  | ||||||
|       running: true |  | ||||||
|     , port: addr.port |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function start(port) { |  | ||||||
|     if (server) { |  | ||||||
|       return curState(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     server = require('socksv5').createServer(function (info, accept) { |  | ||||||
|       accept(); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // It would be nice if we could use `server-destroy` here, but we can't because
 |  | ||||||
|     // the socksv5 library will not give us access to any sockets it actually
 |  | ||||||
|     // handles, so we have no way of keeping track of them or closing them.
 |  | ||||||
|     server.on('close', function () { |  | ||||||
|       server = null; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     server.useAuth(require('socksv5').auth.None()); |  | ||||||
| 
 |  | ||||||
|     return new PromiseA(function (resolve, reject) { |  | ||||||
|       server.on('error', function (err) { |  | ||||||
|         if (!port && err.code === 'EADDRINUSE') { |  | ||||||
|           server.listen(0); |  | ||||||
|         } else { |  | ||||||
|           server = null; |  | ||||||
|           reject(err); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|       server.listen(port || 1080, function () { |  | ||||||
|         resolve(curState()); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function stop() { |  | ||||||
|     if (!server) { |  | ||||||
|       return curState(); |  | ||||||
|     } |  | ||||||
|     return new PromiseA(function (resolve, reject) { |  | ||||||
|       server.close(function (err) { |  | ||||||
|         if (err) { |  | ||||||
|           reject(err); |  | ||||||
|         } else { |  | ||||||
|           resolve(curState()); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var configEnabled = false; |  | ||||||
|   function updateConf() { |  | ||||||
|     var wanted = config.socks5 && config.socks5.enabled; |  | ||||||
| 
 |  | ||||||
|     if (configEnabled && !wanted) { |  | ||||||
|       stop().catch(function (err) { |  | ||||||
|         console.error('failed to stop socks5 proxy on config change', err); |  | ||||||
|       }); |  | ||||||
|       configEnabled = false; |  | ||||||
|     } |  | ||||||
|     if (wanted && !configEnabled) { |  | ||||||
|       start(config.socks5.port).catch(function (err) { |  | ||||||
|         console.error('failed to start Socks5 proxy', err); |  | ||||||
|       }); |  | ||||||
|       configEnabled = true; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   process.nextTick(updateConf); |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     curState |  | ||||||
|   , start |  | ||||||
|   , stop |  | ||||||
|   , updateConf |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										225
									
								
								lib/storage.js
									
									
									
									
									
								
							
							
						
						
									
										225
									
								
								lib/storage.js
									
									
									
									
									
								
							| @ -1,225 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var PromiseA = require('bluebird'); |  | ||||||
| var path = require('path'); |  | ||||||
| var fs = PromiseA.promisifyAll(require('fs')); |  | ||||||
| var jwt = require('jsonwebtoken'); |  | ||||||
| var crypto = require('crypto'); |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, conf) { |  | ||||||
|   var hrIds = require('human-readable-ids').humanReadableIds; |  | ||||||
|   var scmp = require('scmp'); |  | ||||||
|   var storageDir = path.join(__dirname, '..', 'var'); |  | ||||||
| 
 |  | ||||||
|   function read(fileName) { |  | ||||||
|     return fs.readFileAsync(path.join(storageDir, fileName)) |  | ||||||
|     .then(JSON.parse, function (err) { |  | ||||||
|       if (err.code === 'ENOENT') { |  | ||||||
|         return {}; |  | ||||||
|       } |  | ||||||
|       throw err; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   function write(fileName, obj) { |  | ||||||
|     return fs.mkdirAsync(storageDir).catch(function (err) { |  | ||||||
|       if (err.code !== 'EEXIST') { |  | ||||||
|         console.error('failed to mkdir', storageDir, err.toString()); |  | ||||||
|       } |  | ||||||
|     }).then(function () { |  | ||||||
|       return fs.writeFileAsync(path.join(storageDir, fileName), JSON.stringify(obj), 'utf8'); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var owners = { |  | ||||||
|     _filename: 'owners.json' |  | ||||||
|   , all: function () { |  | ||||||
|       return read(this._filename).then(function (owners) { |  | ||||||
|         return Object.keys(owners).map(function (id) { |  | ||||||
|           var owner = owners[id]; |  | ||||||
|           owner.id = id; |  | ||||||
|           return owner; |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , get: function (id) { |  | ||||||
|       // While we could directly read the owners file and access the id directly from
 |  | ||||||
|       // the resulting object I'm not sure of the details of how the object key lookup
 |  | ||||||
|       // works or whether that would expose us to timing attacks.
 |  | ||||||
|       // See https://codahale.com/a-lesson-in-timing-attacks/
 |  | ||||||
|       return this.all().then(function (owners) { |  | ||||||
|         return owners.filter(function (owner) { |  | ||||||
|           return scmp(id, owner.id); |  | ||||||
|         })[0]; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , exists: function (id) { |  | ||||||
|       return this.get(id).then(function (owner) { |  | ||||||
|         return !!owner; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , set: function (id, obj) { |  | ||||||
|       var self = this; |  | ||||||
|       return read(self._filename).then(function (owners) { |  | ||||||
|         obj.id = id; |  | ||||||
|         owners[id] = obj; |  | ||||||
|         return write(self._filename, owners); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   var confCb; |  | ||||||
|   var config = { |  | ||||||
|     save: function (changes) { |  | ||||||
|       deps.messenger.send({ |  | ||||||
|         type: 'com.daplie.goldilocks/config' |  | ||||||
|       , changes: changes |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       return new deps.PromiseA(function (resolve, reject) { |  | ||||||
|         var timeoutId = setTimeout(function () { |  | ||||||
|           reject(new Error('Did not receive config update from main process in a reasonable time')); |  | ||||||
|           confCb = null; |  | ||||||
|         }, 15*1000); |  | ||||||
| 
 |  | ||||||
|         confCb = function (config) { |  | ||||||
|           confCb = null; |  | ||||||
|           clearTimeout(timeoutId); |  | ||||||
|           resolve(config); |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|   function updateConf(config) { |  | ||||||
|     if (confCb) { |  | ||||||
|       confCb(config); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var userTokens = { |  | ||||||
|     _filename: 'user-tokens.json' |  | ||||||
|   , _cache: {} |  | ||||||
|   , _convertToken: function convertToken(id, token) { |  | ||||||
|       // convert the token into something that looks more like what OAuth3 uses internally
 |  | ||||||
|       // as sessions so we can use it with OAuth3. We don't use OAuth3's internal session
 |  | ||||||
|       // storage because it effectively only supports storing tokens based on provider URI.
 |  | ||||||
|       // We also use the token as the `access_token` instead of `refresh_token` because the
 |  | ||||||
|       // refresh functionality is closely tied to the storage.
 |  | ||||||
|       var decoded = jwt.decode(token); |  | ||||||
|       if (!decoded) { |  | ||||||
|         return null; |  | ||||||
|       } |  | ||||||
|       return { |  | ||||||
|         id:           id |  | ||||||
|       , access_token: token |  | ||||||
|       , token:        decoded |  | ||||||
|       , provider_uri: decoded.iss || decoded.issuer || decoded.provider_uri |  | ||||||
|       , client_uri:   decoded.azp |  | ||||||
|       , scope:        decoded.scp || decoded.scope || decoded.grants |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|   , all: function allUserTokens() { |  | ||||||
|       var self = this; |  | ||||||
|       if (self._cacheComplete) { |  | ||||||
|         return deps.PromiseA.resolve(Object.values(self._cache)); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return read(self._filename).then(function (tokens) { |  | ||||||
|         // We will read every single token into our cache, so it will be complete once we finish
 |  | ||||||
|         // creating the result (it's set out of order so we can directly return the result).
 |  | ||||||
|         self._cacheComplete = true; |  | ||||||
| 
 |  | ||||||
|         return Object.keys(tokens).map(function (id) { |  | ||||||
|           self._cache[id] = self._convertToken(id, tokens[id]); |  | ||||||
|           return self._cache[id]; |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , get: function getUserToken(id) { |  | ||||||
|       var self = this; |  | ||||||
|       if (self._cache.hasOwnProperty(id) || self._cacheComplete) { |  | ||||||
|         return deps.PromiseA.resolve(self._cache[id] || null); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return read(self._filename).then(function (tokens) { |  | ||||||
|         self._cache[id] = self._convertToken(id, tokens[id]); |  | ||||||
|         return self._cache[id]; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , save: function saveUserToken(newToken) { |  | ||||||
|       var self = this; |  | ||||||
|       return read(self._filename).then(function (tokens) { |  | ||||||
|         var rawToken; |  | ||||||
|         if (typeof newToken === 'string') { |  | ||||||
|           rawToken = newToken; |  | ||||||
|         } else { |  | ||||||
|           rawToken = newToken.refresh_token || newToken.access_token; |  | ||||||
|         } |  | ||||||
|         if (typeof rawToken !== 'string') { |  | ||||||
|           throw new Error('cannot save invalid session: missing refresh_token and access_token'); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         var decoded = jwt.decode(rawToken); |  | ||||||
|         var idHash = crypto.createHash('sha256'); |  | ||||||
|         idHash.update(decoded.sub || decoded.ppid || decoded.appScopedId || ''); |  | ||||||
|         idHash.update(decoded.iss || decoded.issuer || ''); |  | ||||||
|         idHash.update(decoded.aud || decoded.audience || ''); |  | ||||||
| 
 |  | ||||||
|         var scope = decoded.scope || decoded.scp || decoded.grants || ''; |  | ||||||
|         idHash.update(scope.split(/[,\s]+/mg).sort().join(',')); |  | ||||||
| 
 |  | ||||||
|         var id = idHash.digest('hex'); |  | ||||||
|         tokens[id] = rawToken; |  | ||||||
|         return write(self._filename, tokens).then(function () { |  | ||||||
|           // Delete the current cache so that if this is an update it will refresh
 |  | ||||||
|           // the cache once we read the ID.
 |  | ||||||
|           delete self._cache[id]; |  | ||||||
|           return self.get(id); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , remove: function removeUserToken(id) { |  | ||||||
|       var self = this; |  | ||||||
|       return read(self._filename).then(function (tokens) { |  | ||||||
|         var present = delete tokens[id]; |  | ||||||
|         if (!present) { |  | ||||||
|           return present; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return write(self._filename, tokens).then(function () { |  | ||||||
|           delete self._cache[id]; |  | ||||||
|           return true; |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   var mdnsId = { |  | ||||||
|     _filename: 'mdns-id' |  | ||||||
|   , get: function () { |  | ||||||
|       var self = this; |  | ||||||
|       return read("mdns-id").then(function (result) { |  | ||||||
|         if (typeof result !== 'string') { |  | ||||||
|           throw new Error('mDNS ID not present'); |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|       }).catch(function () { |  | ||||||
|         return self.set(hrIds.random()); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|   , set: function (value) { |  | ||||||
|       var self = this; |  | ||||||
|       return write(self._filename, value).then(function () { |  | ||||||
|         return self.get(); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     owners: owners |  | ||||||
|   , config: config |  | ||||||
|   , updateConf: updateConf |  | ||||||
|   , tokens: userTokens |  | ||||||
|   , mdnsId: mdnsId |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										543
									
								
								lib/tcp/http.js
									
									
									
									
									
								
							
							
						
						
									
										543
									
								
								lib/tcp/http.js
									
									
									
									
									
								
							| @ -1,543 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, conf, tcpMods) { |  | ||||||
|   var PromiseA = require('bluebird'); |  | ||||||
|   var statAsync = PromiseA.promisify(require('fs').stat); |  | ||||||
|   var domainMatches = require('../domain-utils').match; |  | ||||||
|   var separatePort = require('../domain-utils').separatePort; |  | ||||||
| 
 |  | ||||||
|   function parseHeaders(conn, opts) { |  | ||||||
|     // There should already be a `firstChunk` on the opts, but because we might sometimes
 |  | ||||||
|     // need more than that to get all the headers it's easier to always read the data off
 |  | ||||||
|     // the connection and put it back later if we need to.
 |  | ||||||
|     opts.firstChunk = Buffer.alloc(0); |  | ||||||
| 
 |  | ||||||
|     // First we make sure we have all of the headers.
 |  | ||||||
|     return new PromiseA(function (resolve, reject) { |  | ||||||
|       if (opts.firstChunk.includes('\r\n\r\n')) { |  | ||||||
|         resolve(opts.firstChunk.toString()); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       var errored = false; |  | ||||||
|       function handleErr(err) { |  | ||||||
|         errored = true; |  | ||||||
|         reject(err); |  | ||||||
|       } |  | ||||||
|       conn.once('error', handleErr); |  | ||||||
| 
 |  | ||||||
|       function handleChunk(chunk) { |  | ||||||
|         if (!errored) { |  | ||||||
|           opts.firstChunk = Buffer.concat([opts.firstChunk, chunk]); |  | ||||||
|           if (!opts.firstChunk.includes('\r\n\r\n')) { |  | ||||||
|             conn.once('data', handleChunk); |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           conn.removeListener('error', handleErr); |  | ||||||
|           conn.pause(); |  | ||||||
|           resolve(opts.firstChunk.toString()); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       conn.once('data', handleChunk); |  | ||||||
|     }).then(function (firstStr) { |  | ||||||
|       var headerSection = firstStr.split('\r\n\r\n')[0]; |  | ||||||
|       var lines = headerSection.split('\r\n'); |  | ||||||
|       var result = {}; |  | ||||||
| 
 |  | ||||||
|       lines.slice(1).forEach(function (line) { |  | ||||||
|         var match = /([^:]*?)\s*:\s*(.*)/.exec(line); |  | ||||||
|         if (match) { |  | ||||||
|           result[match[1].toLowerCase()] = match[2]; |  | ||||||
|         } else { |  | ||||||
|           console.error('HTTP header line does not match pattern', line); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       var match = /^([a-zA-Z]+)\s+(\S+)\s+HTTP/.exec(lines[0]); |  | ||||||
|       if (!match) { |  | ||||||
|         throw new Error('first line of "HTTP" does not match pattern: '+lines[0]); |  | ||||||
|       } |  | ||||||
|       result.method = match[1].toUpperCase(); |  | ||||||
|       result.url = match[2]; |  | ||||||
| 
 |  | ||||||
|       return result; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function hostMatchesDomains(req, domainList) { |  | ||||||
|     var host = separatePort((req.headers || req).host).host.toLowerCase(); |  | ||||||
| 
 |  | ||||||
|     return domainList.some(function (pattern) { |  | ||||||
|       return domainMatches(pattern, host); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function determinePrimaryHost() { |  | ||||||
|     var result; |  | ||||||
|     if (Array.isArray(conf.domains)) { |  | ||||||
|       conf.domains.some(function (dom) { |  | ||||||
|         if (!dom.modules || !dom.modules.http) { |  | ||||||
|           return false; |  | ||||||
|         } |  | ||||||
|         return dom.names.some(function (domain) { |  | ||||||
|           if (domain[0] !== '*') { |  | ||||||
|             result = domain; |  | ||||||
|             return true; |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     if (result) { |  | ||||||
|       return result; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (Array.isArray(conf.http.modules)) { |  | ||||||
|       conf.http.modules.some(function (mod) { |  | ||||||
|         return mod.domains.some(function (domain) { |  | ||||||
|           if (domain[0] !== '*') { |  | ||||||
|             result = domain; |  | ||||||
|             return true; |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // We handle both HTTPS and HTTP traffic on the same ports, and we want to redirect
 |  | ||||||
|   // any unencrypted requests to the same port they came from unless it came in on
 |  | ||||||
|   // the default HTTP port, in which case there wont be a port specified in the host.
 |  | ||||||
|   var redirecters = {}; |  | ||||||
|   var ipv4Re = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; |  | ||||||
|   var ipv6Re = /^\[[0-9a-fA-F:]+\]$/; |  | ||||||
|   function redirectHttps(req, res) { |  | ||||||
|     var host = separatePort(req.headers.host); |  | ||||||
| 
 |  | ||||||
|     if (!redirecters[host.port]) { |  | ||||||
|       redirecters[host.port] = require('redirect-https')({ port: host.port }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // localhost and IP addresses cannot have real SSL certs (and don't contain any useful
 |  | ||||||
|     // info for redirection either), so we direct some hosts to either localhost.daplie.me
 |  | ||||||
|     // or the "primary domain" ie the first manually specified domain.
 |  | ||||||
|     if (host.host === 'localhost') { |  | ||||||
|       req.headers.host = 'localhost.daplie.me' + (host.port ? ':'+host.port : ''); |  | ||||||
|     } |  | ||||||
|     // Test for IPv4 and IPv6 addresses. These patterns will match some invalid addresses,
 |  | ||||||
|     // but since those still won't be valid domains that won't really be a problem.
 |  | ||||||
|     if (ipv4Re.test(host.host) || ipv6Re.test(host.host)) { |  | ||||||
|       var dest; |  | ||||||
|       if (conf.http.primaryDomain) { |  | ||||||
|         dest = conf.http.primaryDomain; |  | ||||||
|       } else { |  | ||||||
|         dest = determinePrimaryHost(); |  | ||||||
|       } |  | ||||||
|       if (dest) { |  | ||||||
|         req.headers.host = dest + (host.port ? ':'+host.port : ''); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     redirecters[host.port](req, res); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function emitConnection(server, conn, opts) { |  | ||||||
|     server.emit('connection', conn); |  | ||||||
| 
 |  | ||||||
|     // We need to put back whatever data we read off to determine the connection was HTTP
 |  | ||||||
|     // and to parse the headers. Must be done after data handlers added but before any new
 |  | ||||||
|     // data comes in.
 |  | ||||||
|     process.nextTick(function () { |  | ||||||
|       conn.unshift(opts.firstChunk); |  | ||||||
|       conn.resume(); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Convenience return for all the check* functions.
 |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var acmeServer; |  | ||||||
|   function checkAcme(conn, opts, headers) { |  | ||||||
|     if (headers.url.indexOf('/.well-known/acme-challenge/') !== 0) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (deps.stunneld.isClientDomain(separatePort(headers.host).host)) { |  | ||||||
|       deps.stunneld.handleClientConn(conn); |  | ||||||
|       process.nextTick(function () { |  | ||||||
|         conn.unshift(opts.firstChunk); |  | ||||||
|         conn.resume(); |  | ||||||
|       }); |  | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!acmeServer) { |  | ||||||
|       acmeServer = require('http').createServer(tcpMods.tls.middleware); |  | ||||||
|     } |  | ||||||
|     return emitConnection(acmeServer, conn, opts); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function checkLoopback(conn, opts, headers) { |  | ||||||
|     if (headers.url.indexOf('/.well-known/cloud-challenge/') !== 0) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     return emitConnection(deps.ddns.loopbackServer, conn, opts); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var httpsRedirectServer; |  | ||||||
|   function checkHttps(conn, opts, headers) { |  | ||||||
|     if (conf.http.allowInsecure || conn.encrypted) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     if (conf.http.trustProxy && 'https' === headers['x-forwarded-proto']) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!httpsRedirectServer) { |  | ||||||
|       httpsRedirectServer = require('http').createServer(redirectHttps); |  | ||||||
|     } |  | ||||||
|     return emitConnection(httpsRedirectServer, conn, opts); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var adminDomains; |  | ||||||
|   var adminServer; |  | ||||||
|   function checkAdmin(conn, opts, headers) { |  | ||||||
|     var host = separatePort(headers.host).host; |  | ||||||
| 
 |  | ||||||
|     if (!adminDomains) { |  | ||||||
|       adminDomains = require('../admin').adminDomains; |  | ||||||
|     } |  | ||||||
|     if (adminDomains.indexOf(host) !== -1) { |  | ||||||
|       if (!adminServer) { |  | ||||||
|         adminServer = require('../admin').create(deps, conf); |  | ||||||
|       } |  | ||||||
|       return emitConnection(adminServer, conn, opts); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (deps.stunneld.isAdminDomain(host)) { |  | ||||||
|       deps.stunneld.handleAdminConn(conn); |  | ||||||
|       process.nextTick(function () { |  | ||||||
|         conn.unshift(opts.firstChunk); |  | ||||||
|         conn.resume(); |  | ||||||
|       }); |  | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var proxyServer; |  | ||||||
|   function createProxyServer() { |  | ||||||
|     var http = require('http'); |  | ||||||
|     var agent = new http.Agent(); |  | ||||||
|     agent.createConnection = deps.net.createConnection; |  | ||||||
| 
 |  | ||||||
|     var proxy = require('http-proxy').createProxyServer({ |  | ||||||
|       agent: agent |  | ||||||
|     , toProxy: true |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     proxy.on('error', function (err, req, res) { |  | ||||||
|       res.statusCode = 502; |  | ||||||
|       res.setHeader('Connection', 'close'); |  | ||||||
|       res.setHeader('Content-Type', 'text/html'); |  | ||||||
|       res.end(tcpMods.proxy.getRespBody(err, conf.debug)); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     proxyServer = http.createServer(function (req, res) { |  | ||||||
|       proxy.web(req, res, req.connection.proxyOpts); |  | ||||||
|     }); |  | ||||||
|     proxyServer.on('upgrade', function (req, socket, head) { |  | ||||||
|       proxy.ws(req, socket, head, socket.proxyOpts); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   function proxyRequest(mod, conn, opts, xHeaders) { |  | ||||||
|     if (!proxyServer) { |  | ||||||
|       createProxyServer(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     conn.proxyOpts = { |  | ||||||
|       target: 'http://'+(mod.address || (mod.host || 'localhost')+':'+mod.port) |  | ||||||
|     , headers: xHeaders |  | ||||||
|     }; |  | ||||||
|     return emitConnection(proxyServer, conn, opts); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function proxyWebsocket(mod, conn, opts, headers, xHeaders) { |  | ||||||
|     var index = opts.firstChunk.indexOf('\r\n\r\n'); |  | ||||||
|     var body = opts.firstChunk.slice(index); |  | ||||||
| 
 |  | ||||||
|     var head = opts.firstChunk.slice(0, index).toString(); |  | ||||||
|     var headLines = head.split('\r\n'); |  | ||||||
|     // First strip any existing `X-Forwarded-*` headers (for security purposes?)
 |  | ||||||
|     headLines = headLines.filter(function (line) { |  | ||||||
|       return !/^x-forwarded/i.test(line); |  | ||||||
|     }); |  | ||||||
|     // Then add our own `X-Forwarded` headers at the end.
 |  | ||||||
|     Object.keys(xHeaders).forEach(function (key) { |  | ||||||
|       headLines.push(key + ': ' +xHeaders[key]); |  | ||||||
|     }); |  | ||||||
|     // Then convert all of the head lines back into a header buffer.
 |  | ||||||
|     head = Buffer.from(headLines.join('\r\n')); |  | ||||||
| 
 |  | ||||||
|     opts.firstChunk = Buffer.concat([head, body]); |  | ||||||
| 
 |  | ||||||
|     var newConnOpts = separatePort(mod.address || ''); |  | ||||||
|     newConnOpts.port = newConnOpts.port || mod.port; |  | ||||||
|     newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; |  | ||||||
|     newConnOpts.servername = separatePort(headers.host).host; |  | ||||||
|     newConnOpts.data = opts.firstChunk; |  | ||||||
| 
 |  | ||||||
|     newConnOpts.remoteFamily  = opts.family  || conn.remoteFamily; |  | ||||||
|     newConnOpts.remoteAddress = opts.address || conn.remoteAddress; |  | ||||||
|     newConnOpts.remotePort    = opts.port    || conn.remotePort; |  | ||||||
| 
 |  | ||||||
|     tcpMods.proxy(conn, newConnOpts, opts.firstChunk); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function checkProxy(mod, conn, opts, headers) { |  | ||||||
|     var xHeaders = {}; |  | ||||||
|     // Then add our own `X-Forwarded` headers at the end.
 |  | ||||||
|     if (conf.http.trustProxy && headers['x-forwarded-proto']) { |  | ||||||
|       xHeaders['X-Forwarded-Proto'] = headers['x-forwarded-proto']; |  | ||||||
|     } else { |  | ||||||
|       xHeaders['X-Forwarded-Proto'] = conn.encrypted ? 'https' : 'http'; |  | ||||||
|     } |  | ||||||
|     var proxyChain = (headers['x-forwarded-for'] || '').split(/ *, */).filter(Boolean); |  | ||||||
|     proxyChain.push(opts.remoteAddress || opts.address || conn.remoteAddress); |  | ||||||
|     xHeaders['X-Forwarded-For'] = proxyChain.join(', '); |  | ||||||
|     xHeaders['X-Forwarded-Host'] = headers.host; |  | ||||||
| 
 |  | ||||||
|     if ((headers.connection || '').toLowerCase() === 'upgrade') { |  | ||||||
|       proxyWebsocket(mod, conn, opts, headers, xHeaders); |  | ||||||
|     } else { |  | ||||||
|       proxyRequest(mod, conn, opts, xHeaders); |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function checkRedirect(mod, conn, opts, headers) { |  | ||||||
|     if (!mod.fromRe || mod.fromRe.origSrc !== mod.from) { |  | ||||||
|       // Escape any characters that (can) have special meaning in regular expression
 |  | ||||||
|       // but that aren't the special characters we have interest in.
 |  | ||||||
|       var from = mod.from.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); |  | ||||||
|       // Then modify the characters we are interested in so they do what we want in
 |  | ||||||
|       // the regular expression after being compiled.
 |  | ||||||
|       from = from.replace(/\*/g, '(.*)'); |  | ||||||
|       var fromRe = new RegExp('^' + from + '/?$'); |  | ||||||
|       fromRe.origSrc = mod.from; |  | ||||||
|       // We don't want this property showing up in the actual config file or the API,
 |  | ||||||
|       // so we define it this way so it's not enumberable.
 |  | ||||||
|       Object.defineProperty(mod, 'fromRe', {value: fromRe, configurable: true}); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var match = mod.fromRe.exec(headers.url); |  | ||||||
|     if (!match) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var to = mod.to; |  | ||||||
|     match.slice(1).forEach(function (globMatch, index) { |  | ||||||
|       to = to.replace(':'+(index+1), globMatch); |  | ||||||
|     }); |  | ||||||
|     var status = mod.status || 301; |  | ||||||
|     var code = require('http').STATUS_CODES[status] || 'Unknown'; |  | ||||||
| 
 |  | ||||||
|     conn.end([ |  | ||||||
|       'HTTP/1.1 ' + status + ' ' + code |  | ||||||
|     , 'Date: ' + (new Date()).toUTCString() |  | ||||||
|     , 'Location: ' + to |  | ||||||
|     , 'Connection: close' |  | ||||||
|     , 'Content-Length: 0' |  | ||||||
|     , '' |  | ||||||
|     , '' |  | ||||||
|     ].join('\r\n')); |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var staticServer; |  | ||||||
|   var staticHandlers = {}; |  | ||||||
|   var indexHandlers = {}; |  | ||||||
|   function serveStatic(req, res) { |  | ||||||
|     var rootDir = req.connection.rootDir; |  | ||||||
|     var modOpts = req.connection.modOpts; |  | ||||||
| 
 |  | ||||||
|     if (!staticHandlers[rootDir]) { |  | ||||||
|       staticHandlers[rootDir] = require('express').static(rootDir, { |  | ||||||
|         dotfiles: modOpts.dotfiles |  | ||||||
|       , fallthrough: false |  | ||||||
|       , redirect: modOpts.redirect |  | ||||||
|       , index: modOpts.index |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     staticHandlers[rootDir](req, res, function (err) { |  | ||||||
|       function doFinal() { |  | ||||||
|         if (err) { |  | ||||||
|           res.statusCode = err.statusCode; |  | ||||||
|         } else { |  | ||||||
|           res.statusCode = 404; |  | ||||||
|         } |  | ||||||
|         res.setHeader('Content-Type', 'text/html'); |  | ||||||
| 
 |  | ||||||
|         if (res.statusCode === 404) { |  | ||||||
|           res.end('File Not Found'); |  | ||||||
|         } else { |  | ||||||
|           res.end(require('http').STATUS_CODES[res.statusCode]); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       var handlerHandle = rootDir |  | ||||||
|         + (modOpts.hidden||'') |  | ||||||
|         + (modOpts.icons||'') |  | ||||||
|         + (modOpts.stylesheet||'') |  | ||||||
|         + (modOpts.template||'') |  | ||||||
|         + (modOpts.view||'') |  | ||||||
|         ; |  | ||||||
| 
 |  | ||||||
|       function pathMatchesUrl(pathname) { |  | ||||||
|         if (req.url === pathname) { |  | ||||||
|           return true; |  | ||||||
|         } |  | ||||||
|         if (0 === req.url.replace(/\/?$/, '/').indexOf(pathname.replace(/\/?$/, '/'))) { |  | ||||||
|           return true; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (!modOpts.indexes || ('*' !== modOpts.indexes[0] && !modOpts.indexes.some(pathMatchesUrl))) { |  | ||||||
|         doFinal(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (!indexHandlers[handlerHandle]) { |  | ||||||
|         // https://www.npmjs.com/package/serve-index
 |  | ||||||
|         indexHandlers[handlerHandle] = require('serve-index')(rootDir, { |  | ||||||
|           hidden: modOpts.hidden |  | ||||||
|         , icons: modOpts.icons |  | ||||||
|         , stylesheet: modOpts.stylesheet |  | ||||||
|         , template: modOpts.template |  | ||||||
|         , view: modOpts.view |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       indexHandlers[handlerHandle](req, res, function (_err) { |  | ||||||
|         err = _err || err; |  | ||||||
| 
 |  | ||||||
|         doFinal(); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   function checkStatic(modOpts, conn, opts, headers) { |  | ||||||
|     var rootDir = modOpts.root.replace(':hostname', separatePort(headers.host).host); |  | ||||||
|     return statAsync(rootDir) |  | ||||||
|       .then(function (stats) { |  | ||||||
|         if (!stats || !stats.isDirectory()) { |  | ||||||
|           return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!staticServer) { |  | ||||||
|           staticServer = require('http').createServer(serveStatic); |  | ||||||
|         } |  | ||||||
|         conn.rootDir = rootDir; |  | ||||||
|         conn.modOpts = modOpts; |  | ||||||
|         return emitConnection(staticServer, conn, opts); |  | ||||||
|       }) |  | ||||||
|       .catch(function (err) { |  | ||||||
|         if (err.code !== 'ENOENT') { |  | ||||||
|           console.warn('errored stating', rootDir, 'for serving static files', err); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|       }) |  | ||||||
|       ; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // The function signature is as follows
 |  | ||||||
|   // function module(moduleOptions, tcpConnection, connectionOptions, headers) { ... }
 |  | ||||||
|   var moduleChecks = { |  | ||||||
|     proxy:    checkProxy |  | ||||||
|   , redirect: checkRedirect |  | ||||||
|   , static:   checkStatic |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   function handleConnection(conn) { |  | ||||||
|     var opts = conn.__opts; |  | ||||||
|     parseHeaders(conn, opts) |  | ||||||
|       .then(function (headers) { |  | ||||||
|         if (checkAcme(conn, opts, headers))  { return; } |  | ||||||
|         if (checkLoopback(conn, opts, headers))  { return; } |  | ||||||
|         if (checkHttps(conn, opts, headers)) { return; } |  | ||||||
|         if (checkAdmin(conn, opts, headers)) { return; } |  | ||||||
| 
 |  | ||||||
|         var prom = PromiseA.resolve(false); |  | ||||||
|         (conf.domains || []).forEach(function (dom) { |  | ||||||
|           prom = prom.then(function (handled) { |  | ||||||
|             if (handled) { |  | ||||||
|               return handled; |  | ||||||
|             } |  | ||||||
|             if (!dom.modules || !dom.modules.http) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             if (!hostMatchesDomains(headers, dom.names)) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             var subProm = PromiseA.resolve(false); |  | ||||||
|             dom.modules.http.forEach(function (mod) { |  | ||||||
|               if (moduleChecks[mod.type]) { |  | ||||||
|                 subProm = subProm.then(function (handled) { |  | ||||||
|                   if (handled) { return handled; } |  | ||||||
|                   return moduleChecks[mod.type](mod, conn, opts, headers); |  | ||||||
|                 }); |  | ||||||
|               } else { |  | ||||||
|                 console.warn('unknown HTTP module under domains', dom.names.join(','), mod); |  | ||||||
|               } |  | ||||||
|             }); |  | ||||||
|             return subProm; |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|         (conf.http.modules || []).forEach(function (mod) { |  | ||||||
|           prom = prom.then(function (handled) { |  | ||||||
|             if (handled) { |  | ||||||
|               return handled; |  | ||||||
|             } |  | ||||||
|             if (!hostMatchesDomains(headers, mod.domains)) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (moduleChecks[mod.type]) { |  | ||||||
|               return moduleChecks[mod.type](mod, conn, opts, headers); |  | ||||||
|             } |  | ||||||
|             console.warn('unknown HTTP module found', mod); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         prom.then(function (handled) { |  | ||||||
|           // XXX TODO SECURITY html escape
 |  | ||||||
|           var host = (headers.host || '[no host header]').replace(/</, '<'); |  | ||||||
|           // TODO specify filepath of config file or database connection, etc
 |  | ||||||
|           var msg = "Bad Gateway: Goldilocks accepted '" + host + "' but no module (neither static nor proxy) was designated to handle it. Check your config file."; |  | ||||||
|           if (!handled) { |  | ||||||
|             conn.end([ |  | ||||||
|               'HTTP/1.1 502 Bad Gateway' |  | ||||||
|             , 'Date: ' + (new Date()).toUTCString() |  | ||||||
|             , 'Content-Type: text/html' |  | ||||||
|             , 'Content-Length: ' + msg.length |  | ||||||
|             , 'Connection: close' |  | ||||||
|             , '' |  | ||||||
|             , msg |  | ||||||
|             ].join('\r\n')); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }) |  | ||||||
|       ; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     emit: function (type, value) { |  | ||||||
|       if (type === 'connection') { |  | ||||||
|         handleConnection(value); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										242
									
								
								lib/tcp/index.js
									
									
									
									
									
								
							
							
						
						
									
										242
									
								
								lib/tcp/index.js
									
									
									
									
									
								
							| @ -1,242 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config) { |  | ||||||
|   console.log('config', config); |  | ||||||
| 
 |  | ||||||
|   var listeners = require('../servers').listeners.tcp; |  | ||||||
|   var domainUtils = require('../domain-utils'); |  | ||||||
|   var modules; |  | ||||||
| 
 |  | ||||||
|   var addrProperties = [ |  | ||||||
|     'remoteAddress' |  | ||||||
|   , 'remotePort' |  | ||||||
|   , 'remoteFamily' |  | ||||||
|   , 'localAddress' |  | ||||||
|   , 'localPort' |  | ||||||
|   , 'localFamily' |  | ||||||
|   ]; |  | ||||||
| 
 |  | ||||||
|   function nameMatchesDomains(name, domainList) { |  | ||||||
|     return domainList.some(function (pattern) { |  | ||||||
|       return domainUtils.match(pattern, name); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function proxy(mod, conn, opts) { |  | ||||||
|     // First thing we need to add to the connection options is where to proxy the connection to
 |  | ||||||
|     var newConnOpts = domainUtils.separatePort(mod.address || ''); |  | ||||||
|     newConnOpts.port = newConnOpts.port || mod.port; |  | ||||||
|     newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; |  | ||||||
| 
 |  | ||||||
|     // Then we add all of the connection address information. We need to prefix all of the
 |  | ||||||
|     // properties with '_' so we can provide the information to any connection `createConnection`
 |  | ||||||
|     // implementation but not have the default implementation try to bind the same local port.
 |  | ||||||
|     addrProperties.forEach(function (name) { |  | ||||||
|       newConnOpts['_' + name] = opts[name] || opts['_'+name] || conn[name] || conn['_'+name]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     modules.proxy(conn, newConnOpts); |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function checkTcpProxy(conn, opts) { |  | ||||||
|     var proxied = false; |  | ||||||
| 
 |  | ||||||
|     // TCP Proxying (ie routing based on domain name [vs local port]) only works for
 |  | ||||||
|     // TLS wrapped connections, so if the opts don't give us a servername or don't tell us
 |  | ||||||
|     // this is the decrypted side of a TLS connection we can't handle it here.
 |  | ||||||
|     if (!opts.servername || !opts.encrypted) { return proxied; } |  | ||||||
| 
 |  | ||||||
|     proxied = config.domains.some(function (dom) { |  | ||||||
|       if (!dom.modules || !Array.isArray(dom.modules.tcp)) { return false; } |  | ||||||
|       if (!nameMatchesDomains(opts.servername, dom.names)) { return false; } |  | ||||||
| 
 |  | ||||||
|       return dom.modules.tcp.some(function (mod) { |  | ||||||
|         if (mod.type !== 'proxy') { return false; } |  | ||||||
| 
 |  | ||||||
|         return proxy(mod, conn, opts); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     proxied = proxied || config.tcp.modules.some(function (mod) { |  | ||||||
|       if (mod.type !== 'proxy') { return false; } |  | ||||||
|       if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; } |  | ||||||
| 
 |  | ||||||
|       return proxy(mod, conn, opts); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return proxied; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function checkTcpForward(conn, opts) { |  | ||||||
|     // TCP forwarding (ie routing connections based on local port) requires the local port
 |  | ||||||
|     if (!conn.localPort) { return false; } |  | ||||||
| 
 |  | ||||||
|     return config.tcp.modules.some(function (mod) { |  | ||||||
|       if (mod.type !== 'forward')                { return false; } |  | ||||||
|       if (mod.ports.indexOf(conn.localPort) < 0) { return false; } |  | ||||||
| 
 |  | ||||||
|       return proxy(mod, conn, opts); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
 |  | ||||||
|   function peek(conn, firstChunk, opts) { |  | ||||||
|     opts.firstChunk = firstChunk; |  | ||||||
|     conn.__opts = opts; |  | ||||||
|     // TODO port/service-based routing can do here
 |  | ||||||
| 
 |  | ||||||
|     // TLS byte 1 is handshake and byte 6 is client hello
 |  | ||||||
|     if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) { |  | ||||||
|       modules.tls.emit('connection', conn); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // This doesn't work with TLS, but now that we know this isn't a TLS connection we can
 |  | ||||||
|     // unshift the first chunk back onto the connection for future use. The unshift should
 |  | ||||||
|     // happen after any listeners are attached to it but before any new data comes in.
 |  | ||||||
|     if (!opts.hyperPeek) { |  | ||||||
|       process.nextTick(function () { |  | ||||||
|         conn.unshift(firstChunk); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Connection is not TLS, check for HTTP next.
 |  | ||||||
|     if (firstChunk[0] > 32 && firstChunk[0] < 127) { |  | ||||||
|       var firstStr = firstChunk.toString(); |  | ||||||
|       if (/HTTP\//i.test(firstStr)) { |  | ||||||
|         modules.http.emit('connection', conn); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.warn('failed to identify protocol from first chunk', firstChunk); |  | ||||||
|     conn.destroy(); |  | ||||||
|   } |  | ||||||
|   function tcpHandler(conn, opts) { |  | ||||||
|     function getProp(name) { |  | ||||||
|       return opts[name] || opts['_'+name] || conn[name] || conn['_'+name]; |  | ||||||
|     } |  | ||||||
|     opts = opts || {}; |  | ||||||
|     var logName = getProp('remoteAddress') + ':' + getProp('remotePort') + ' -> ' + |  | ||||||
|                   getProp('localAddress')  + ':' + getProp('localPort'); |  | ||||||
|     console.log('[tcpHandler]', logName, 'connection started - encrypted: ' + (opts.encrypted || false)); |  | ||||||
| 
 |  | ||||||
|     var start = Date.now(); |  | ||||||
|     conn.on('timeout', function () { |  | ||||||
|       console.log('[tcpHandler]', logName, 'connection timed out', (Date.now()-start)/1000); |  | ||||||
|     }); |  | ||||||
|     conn.on('end', function () { |  | ||||||
|       console.log('[tcpHandler]', logName, 'connection ended', (Date.now()-start)/1000); |  | ||||||
|     }); |  | ||||||
|     conn.on('close', function () { |  | ||||||
|       console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     if (checkTcpForward(conn, opts)) { return; } |  | ||||||
|     if (checkTcpProxy(conn, opts))   { return; } |  | ||||||
| 
 |  | ||||||
|     // XXX PEEK COMMENT XXX
 |  | ||||||
|     // TODO we can have our cake and eat it too
 |  | ||||||
|     // we can skip the need to wrap the TLS connection twice
 |  | ||||||
|     // because we've already peeked at the data,
 |  | ||||||
|     // but this needs to be handled better before we enable that
 |  | ||||||
|     // (because it creates new edge cases)
 |  | ||||||
|     if (opts.hyperPeek) { |  | ||||||
|       console.log('hyperpeek'); |  | ||||||
|       peek(conn, opts.firstChunk, opts); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function onError(err) { |  | ||||||
|       console.error('[error] socket errored peeking -', err); |  | ||||||
|       conn.destroy(); |  | ||||||
|     } |  | ||||||
|     conn.once('error', onError); |  | ||||||
|     conn.once('data', function (chunk) { |  | ||||||
|       conn.removeListener('error', onError); |  | ||||||
|       peek(conn, chunk, opts); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   process.nextTick(function () { |  | ||||||
|     modules = {}; |  | ||||||
|     modules.tcpHandler = tcpHandler; |  | ||||||
|     modules.proxy = require('./proxy-conn').create(deps, config); |  | ||||||
|     modules.tls   = require('./tls').create(deps, config, modules); |  | ||||||
|     modules.http  = require('./http').create(deps, config, modules); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   function updateListeners() { |  | ||||||
|     var current = listeners.list(); |  | ||||||
|     var wanted = config.tcp.bind; |  | ||||||
| 
 |  | ||||||
|     if (!Array.isArray(wanted)) { wanted = []; } |  | ||||||
|     wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356); |  | ||||||
| 
 |  | ||||||
|     var closeProms = current.filter(function (port) { |  | ||||||
|       return wanted.indexOf(port) < 0; |  | ||||||
|     }).map(function (port) { |  | ||||||
|       return listeners.close(port, 1000); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // We don't really need to filter here since listening on the same port with the
 |  | ||||||
|     // same handler function twice is basically a no-op.
 |  | ||||||
|     var openProms = wanted.map(function (port) { |  | ||||||
|       return listeners.add(port, tcpHandler); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return Promise.all(closeProms.concat(openProms)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var mainPort; |  | ||||||
|   function updateConf() { |  | ||||||
|     updateListeners().catch(function (err) { |  | ||||||
|       console.error('Error updating TCP listeners to match bind configuration'); |  | ||||||
|       console.error(err); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     var unforwarded = {}; |  | ||||||
|     config.tcp.bind.forEach(function (port) { |  | ||||||
|       unforwarded[port] = true; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     config.tcp.modules.forEach(function (mod) { |  | ||||||
|       if (['forward', 'proxy'].indexOf(mod.type) < 0) { |  | ||||||
|         console.warn('unknown TCP module type specified', JSON.stringify(mod)); |  | ||||||
|       } |  | ||||||
|       if (mod.type !== 'forward') { return; } |  | ||||||
| 
 |  | ||||||
|       mod.ports.forEach(function (port) { |  | ||||||
|         if (!unforwarded[port]) { |  | ||||||
|           console.warn('trying to forward TCP port ' + port + ' multiple times or it is unbound'); |  | ||||||
|         } else { |  | ||||||
|           delete unforwarded[port]; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Not really sure what we can reasonably do to prevent this. At least not without making
 |  | ||||||
|     // our configuration validation more complicated.
 |  | ||||||
|     if (!Object.keys(unforwarded).length) { |  | ||||||
|       console.warn('no bound TCP ports are not being forwarded, admin interface will be inaccessible'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // If we are listening on port 443 make that the main port we respond to mDNS queries with
 |  | ||||||
|     // otherwise choose the lowest number port we are bound to but not forwarding.
 |  | ||||||
|     if (unforwarded['443']) { |  | ||||||
|       mainPort = 443; |  | ||||||
|     } else { |  | ||||||
|       mainPort = Object.keys(unforwarded).map(Number).sort((a, b) => a - b)[0]; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   updateConf(); |  | ||||||
| 
 |  | ||||||
|   var result =  { |  | ||||||
|     updateConf |  | ||||||
|   , handler: tcpHandler |  | ||||||
|   }; |  | ||||||
|   Object.defineProperty(result, 'mainPort', {enumerable: true, get: () => mainPort}); |  | ||||||
| 
 |  | ||||||
|   return result; |  | ||||||
| }; |  | ||||||
| @ -1,81 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| function getRespBody(err, debug) { |  | ||||||
|   if (debug) { |  | ||||||
|     return err.toString(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (err.code === 'ECONNREFUSED') { |  | ||||||
|     return 'The connection was refused. Most likely the service being connected to ' |  | ||||||
|       + 'has stopped running or the configuration is wrong.'; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return 'Bad Gateway: ' + err.code; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function sendBadGateway(conn, err, debug) { |  | ||||||
|   var body = getRespBody(err, debug); |  | ||||||
| 
 |  | ||||||
|   conn.write([ |  | ||||||
|     'HTTP/1.1 502 Bad Gateway' |  | ||||||
|   , 'Date: ' + (new Date()).toUTCString() |  | ||||||
|   , 'Connection: close' |  | ||||||
|   , 'Content-Type: text/html' |  | ||||||
|   , 'Content-Length: ' + body.length |  | ||||||
|   , '' |  | ||||||
|   , body |  | ||||||
|   ].join('\r\n')); |  | ||||||
|   conn.end(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports.getRespBody = getRespBody; |  | ||||||
| module.exports.sendBadGateway = sendBadGateway; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config) { |  | ||||||
|   function proxy(conn, newConnOpts, firstChunk, decrypt) { |  | ||||||
|     var connected = false; |  | ||||||
|     newConnOpts.allowHalfOpen = true; |  | ||||||
|     var newConn = deps.net.createConnection(newConnOpts, function () { |  | ||||||
|       connected = true; |  | ||||||
| 
 |  | ||||||
|       if (firstChunk) { |  | ||||||
|         newConn.write(firstChunk); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       newConn.pipe(conn); |  | ||||||
|       conn.pipe(newConn); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Listening for this largely to prevent uncaught exceptions.
 |  | ||||||
|     conn.on('error', function (err) { |  | ||||||
|       console.log('proxy client error', err); |  | ||||||
|     }); |  | ||||||
|     newConn.on('error', function (err) { |  | ||||||
|       if (connected) { |  | ||||||
|         // Not sure how to report this to a user or a client. We can assume that some data
 |  | ||||||
|         // has already been exchanged, so we can't really be sure what we can send in addition
 |  | ||||||
|         // that wouldn't result in a parse error.
 |  | ||||||
|         console.log('proxy remote error', err); |  | ||||||
|       } else { |  | ||||||
|         console.log('proxy connection error', err); |  | ||||||
|         if (decrypt) { |  | ||||||
|           sendBadGateway(decrypt(conn), err, config.debug); |  | ||||||
|         } else { |  | ||||||
|           sendBadGateway(conn, err, config.debug); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Make sure that once one side closes, no I/O activity will happen on the other side.
 |  | ||||||
|     conn.on('close', function () { |  | ||||||
|       newConn.destroy(); |  | ||||||
|     }); |  | ||||||
|     newConn.on('close', function () { |  | ||||||
|       conn.destroy(); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   proxy.getRespBody = getRespBody; |  | ||||||
|   proxy.sendBadGateway = sendBadGateway; |  | ||||||
|   return proxy; |  | ||||||
| }; |  | ||||||
							
								
								
									
										349
									
								
								lib/tcp/tls.js
									
									
									
									
									
								
							
							
						
						
									
										349
									
								
								lib/tcp/tls.js
									
									
									
									
									
								
							| @ -1,349 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config, tcpMods) { |  | ||||||
|   var path = require('path'); |  | ||||||
|   var tls = require('tls'); |  | ||||||
|   var parseSni = require('sni'); |  | ||||||
|   var greenlock = require('greenlock'); |  | ||||||
|   var localhostCerts = require('localhost.daplie.me-certificates'); |  | ||||||
|   var domainMatches = require('../domain-utils').match; |  | ||||||
| 
 |  | ||||||
|   function extractSocketProp(socket, propName) { |  | ||||||
|     // remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854
 |  | ||||||
|     var altName = '_' + propName; |  | ||||||
|     var value = socket[propName] || socket[altName]; |  | ||||||
|     try { |  | ||||||
|       value = value || socket._handle._parent.owner.stream[propName]; |  | ||||||
|       value = value || socket._handle._parent.owner.stream[altName]; |  | ||||||
|     } catch (e) {} |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|       value = value || socket._handle._parentWrap[propName]; |  | ||||||
|       value = value || socket._handle._parentWrap[altName]; |  | ||||||
|       value = value || socket._handle._parentWrap._handle.owner.stream[propName]; |  | ||||||
|       value = value || socket._handle._parentWrap._handle.owner.stream[altName]; |  | ||||||
|     } catch (e) {} |  | ||||||
| 
 |  | ||||||
|     return value || ''; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function nameMatchesDomains(name, domainList) { |  | ||||||
|     return domainList.some(function (pattern) { |  | ||||||
|       return domainMatches(pattern, name); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var addressNames = [ |  | ||||||
|     'remoteAddress' |  | ||||||
|   , 'remotePort' |  | ||||||
|   , 'remoteFamily' |  | ||||||
|   , 'localAddress' |  | ||||||
|   , 'localPort' |  | ||||||
|   ]; |  | ||||||
|   function wrapSocket(socket, opts, cb) { |  | ||||||
|     var reader = require('socket-pair').create(function (err, writer) { |  | ||||||
|       if (typeof cb === 'function') { |  | ||||||
|         process.nextTick(cb); |  | ||||||
|       } |  | ||||||
|       if (err) { |  | ||||||
|         reader.emit('error', err); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       writer.write(opts.firstChunk); |  | ||||||
|       socket.pipe(writer); |  | ||||||
|       writer.pipe(socket); |  | ||||||
| 
 |  | ||||||
|       socket.on('error', function (err) { |  | ||||||
|         console.log('wrapped TLS socket error', err); |  | ||||||
|         reader.emit('error', err); |  | ||||||
|       }); |  | ||||||
|       writer.on('error', function (err) { |  | ||||||
|         console.error('socket-pair writer error', err); |  | ||||||
|         // If the writer had an error the reader probably did too, and I don't think we'll
 |  | ||||||
|         // get much out of emitting this on the original socket, so logging is enough.
 |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       socket.on('close', writer.destroy.bind(writer)); |  | ||||||
|       writer.on('close', socket.destroy.bind(socket)); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // We can't set these properties the normal way because there is a getter without a setter,
 |  | ||||||
|     // but we can use defineProperty. We reuse the descriptor even though we will be manipulating
 |  | ||||||
|     // it because we will only ever set the value and we set it every time.
 |  | ||||||
|     var descriptor = {enumerable: true, configurable: true, writable: true}; |  | ||||||
|     addressNames.forEach(function (name) { |  | ||||||
|       descriptor.value = opts[name] || extractSocketProp(socket, name); |  | ||||||
|       Object.defineProperty(reader, name, descriptor); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return reader; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var le = greenlock.create({ |  | ||||||
|     server: 'https://acme-v01.api.letsencrypt.org/directory' |  | ||||||
| 
 |  | ||||||
|   , challenges: { |  | ||||||
|       'http-01': require('le-challenge-fs').create({ debug: config.debug }) |  | ||||||
|     , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug }) |  | ||||||
|     , 'dns-01': deps.ddns.challenge |  | ||||||
|     } |  | ||||||
|   , challengeType: 'http-01' |  | ||||||
| 
 |  | ||||||
|   , store: require('le-store-certbot').create({ |  | ||||||
|       debug: config.debug |  | ||||||
|     , configDir: path.join(require('os').homedir(), 'acme', 'etc') |  | ||||||
|     , logDir: path.join(require('os').homedir(), 'acme', 'var', 'log') |  | ||||||
|     , workDir: path.join(require('os').homedir(), 'acme', 'var', 'lib') |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|   , approveDomains: function (opts, certs, cb) { |  | ||||||
|       // This is where you check your database and associated
 |  | ||||||
|       // email addresses with domains and agreements and such
 |  | ||||||
| 
 |  | ||||||
|       // The domains being approved for the first time are listed in opts.domains
 |  | ||||||
|       // Certs being renewed are listed in certs.altnames
 |  | ||||||
|       if (certs) { |  | ||||||
|         // TODO make sure the same options are used for renewal as for registration?
 |  | ||||||
|         opts.domains = certs.altnames; |  | ||||||
|         cb(null, { options: opts, certs: certs }); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       function complete(optsOverride, domains) { |  | ||||||
|         if (!cb) { |  | ||||||
|           console.warn('tried to complete domain approval multiple times'); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // // We can't request certificates for wildcard domains, so filter any of those
 |  | ||||||
|         // // out of this list and put the domain that triggered this in the list if needed.
 |  | ||||||
|         // domains = (domains || []).filter(function (dom) { return dom[0] !== '*'; });
 |  | ||||||
|         // if (domains.indexOf(opts.domain) < 0) {
 |  | ||||||
|         //   domains.push(opts.domain);
 |  | ||||||
|         // }
 |  | ||||||
|         domains = [ opts.domain ]; |  | ||||||
|         // TODO: allow user to specify options for challenges or storage.
 |  | ||||||
| 
 |  | ||||||
|         Object.assign(opts, optsOverride, { domains: domains, agreeTos: true }); |  | ||||||
|         cb(null, { options: opts, certs: certs }); |  | ||||||
|         cb = null; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       var handled = false; |  | ||||||
|       if (Array.isArray(config.domains)) { |  | ||||||
|         handled = config.domains.some(function (dom) { |  | ||||||
|           if (!dom.modules || !dom.modules.tls) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           if (!nameMatchesDomains(opts.domain, dom.names)) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           return dom.modules.tls.some(function (mod) { |  | ||||||
|             if (mod.type !== 'acme') { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             complete(mod, dom.names); |  | ||||||
|             return true; |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       if (handled) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (Array.isArray(config.tls.modules)) { |  | ||||||
|         handled = config.tls.modules.some(function (mod) { |  | ||||||
|           if (mod.type !== 'acme') { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           if (!nameMatchesDomains(opts.domain, mod.domains)) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           complete(mod, mod.domains); |  | ||||||
|           return true; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       if (handled) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       cb(new Error('domain is not allowed')); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   le.tlsOptions = le.tlsOptions || le.httpsOptions; |  | ||||||
| 
 |  | ||||||
|   var secureContexts = {}; |  | ||||||
|   var terminatorOpts = require('localhost.daplie.me-certificates').merge({}); |  | ||||||
|   terminatorOpts.SNICallback = function (sni, cb) { |  | ||||||
|     sni = sni.toLowerCase(); |  | ||||||
|     console.log("[tlsOptions.SNICallback] SNI: '" + sni + "'"); |  | ||||||
| 
 |  | ||||||
|     var tlsOptions; |  | ||||||
| 
 |  | ||||||
|     // Static Certs
 |  | ||||||
|     if (/\.invalid$/.test(sni)) { |  | ||||||
|       sni = 'localhost.daplie.me'; |  | ||||||
|     } |  | ||||||
|     if (/.*localhost.*\.daplie\.me/.test(sni)) { |  | ||||||
|       if (!secureContexts[sni]) { |  | ||||||
|         tlsOptions = localhostCerts.mergeTlsOptions(sni, {}); |  | ||||||
|         if (tlsOptions) { |  | ||||||
|           secureContexts[sni] = tls.createSecureContext(tlsOptions); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (secureContexts[sni]) { |  | ||||||
|         console.log('Got static secure context:', sni, secureContexts[sni]); |  | ||||||
|         cb(null, secureContexts[sni]); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     le.tlsOptions.SNICallback(sni, cb); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   var terminateServer = tls.createServer(terminatorOpts, function (socket) { |  | ||||||
|     console.log('(post-terminated) tls connection, addr:', extractSocketProp(socket, 'remoteAddress')); |  | ||||||
| 
 |  | ||||||
|     tcpMods.tcpHandler(socket, { |  | ||||||
|       servername: socket.servername |  | ||||||
|     , encrypted: true |  | ||||||
|       // remoteAddress... ugh... https://github.com/nodejs/node/issues/8854
 |  | ||||||
|     , remoteAddress: extractSocketProp(socket, 'remoteAddress') |  | ||||||
|     , remotePort:    extractSocketProp(socket, 'remotePort') |  | ||||||
|     , remoteFamily:  extractSocketProp(socket, 'remoteFamily') |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|   terminateServer.on('error', function (err) { |  | ||||||
|     console.log('[error] TLS termination server', err); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   function proxy(socket, opts, mod) { |  | ||||||
|     var newConnOpts = require('../domain-utils').separatePort(mod.address || ''); |  | ||||||
|     newConnOpts.port = newConnOpts.port || mod.port; |  | ||||||
|     newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; |  | ||||||
|     newConnOpts.servername = opts.servername; |  | ||||||
|     newConnOpts.data = opts.firstChunk; |  | ||||||
| 
 |  | ||||||
|     newConnOpts.remoteFamily  = opts.family  || extractSocketProp(socket, 'remoteFamily'); |  | ||||||
|     newConnOpts.remoteAddress = opts.address || extractSocketProp(socket, 'remoteAddress'); |  | ||||||
|     newConnOpts.remotePort    = opts.port    || extractSocketProp(socket, 'remotePort'); |  | ||||||
| 
 |  | ||||||
|     tcpMods.proxy(socket, newConnOpts, opts.firstChunk, function () { |  | ||||||
|       // This function is called in the event of a connection error and should decrypt
 |  | ||||||
|       // the socket so the proxy module can send a 502 HTTP response.
 |  | ||||||
|       var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true}); |  | ||||||
|       if (opts.hyperPeek) { |  | ||||||
|         return new tls.TLSSocket(socket, tlsOpts); |  | ||||||
|       } else { |  | ||||||
|         return new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function terminate(socket, opts) { |  | ||||||
|     console.log( |  | ||||||
|       '[tls-terminate]' |  | ||||||
|     , opts.localAddress || socket.localAddress +':'+ opts.localPort || socket.localPort |  | ||||||
|     , 'servername=' + opts.servername |  | ||||||
|     , opts.remoteAddress || socket.remoteAddress |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     var wrapped; |  | ||||||
|     // We can't emit the connection to the TLS server until we know the connection is fully
 |  | ||||||
|     // opened, otherwise it might hang open when the decrypted side is destroyed.
 |  | ||||||
|     // https://github.com/nodejs/node/issues/14605
 |  | ||||||
|     function emitSock() { |  | ||||||
|       terminateServer.emit('connection', wrapped); |  | ||||||
|     } |  | ||||||
|     if (opts.hyperPeek) { |  | ||||||
|       // This connection was peeked at using a method that doesn't interferre with the TLS
 |  | ||||||
|       // server's ability to handle it properly. Currently the only way this happens is
 |  | ||||||
|       // with tunnel connections where we have the first chunk of data before creating the
 |  | ||||||
|       // new connection (thus removing need to get data off the new connection).
 |  | ||||||
|       wrapped = socket; |  | ||||||
|       process.nextTick(emitSock); |  | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|       // The hyperPeek flag wasn't set, so we had to read data off of this connection, which
 |  | ||||||
|       // means we can no longer use it directly in the TLS server.
 |  | ||||||
|       // See https://github.com/nodejs/node/issues/8752 (node's internal networking layer == 💩 sometimes)
 |  | ||||||
|       wrapped = wrapSocket(socket, opts, emitSock); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function handleConn(socket, opts) { |  | ||||||
|     opts.servername = (parseSni(opts.firstChunk)||'').toLowerCase() || 'localhost.invalid'; |  | ||||||
|     // needs to wind up in one of 2 states:
 |  | ||||||
|     // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket)
 |  | ||||||
|     // 2. Terminated (goes on to a particular module or route, including the admin interface)
 |  | ||||||
|     // 3. Closed (we don't recognize the SNI servername as something we actually want to handle)
 |  | ||||||
| 
 |  | ||||||
|     // We always want to terminate is the SNI matches the challenge pattern, unless a client
 |  | ||||||
|     // on the south side has temporarily claimed a particular challenge. For the time being
 |  | ||||||
|     // we don't have a way for the south-side to communicate with us, so that part isn't done.
 |  | ||||||
|     if (domainMatches('*.acme-challenge.invalid', opts.servername)) { |  | ||||||
|       terminate(socket, opts); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (deps.stunneld.isClientDomain(opts.servername)) { |  | ||||||
|       deps.stunneld.handleClientConn(socket); |  | ||||||
|       if (!opts.hyperPeek) { |  | ||||||
|         process.nextTick(function () { |  | ||||||
|           socket.unshift(opts.firstChunk); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function checkModule(mod) { |  | ||||||
|       if (mod.type === 'proxy') { |  | ||||||
|         return proxy(socket, opts, mod); |  | ||||||
|       } |  | ||||||
|       if (mod.type !== 'acme') { |  | ||||||
|         console.error('saw unknown TLS module', mod); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var handled = (config.domains || []).some(function (dom) { |  | ||||||
|       if (!dom.modules || !dom.modules.tls) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|       if (!nameMatchesDomains(opts.servername, dom.names)) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return dom.modules.tls.some(checkModule); |  | ||||||
|     }); |  | ||||||
|     if (handled) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     handled = (config.tls.modules || []).some(function (mod) { |  | ||||||
|       if (!nameMatchesDomains(opts.servername, mod.domains)) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|       return checkModule(mod); |  | ||||||
|     }); |  | ||||||
|     if (handled) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // TODO: figure out all of the domains that the other modules intend to handle, and only
 |  | ||||||
|     // terminate those ones, closing connections for all others.
 |  | ||||||
|     terminate(socket, opts); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     emit: function (type, socket) { |  | ||||||
|       if (type === 'connection') { |  | ||||||
|         handleConn(socket, socket.__opts); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   , middleware: le.middleware() |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,131 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| function httpsTunnel(servername, conn) { |  | ||||||
|   console.error('tunnel server received encrypted connection to', servername); |  | ||||||
|   conn.end(); |  | ||||||
| } |  | ||||||
| function handleHttp(servername, conn) { |  | ||||||
|   console.error('tunnel server received un-encrypted connection to', servername); |  | ||||||
|   conn.end([ |  | ||||||
|     'HTTP/1.1 404 Not Found' |  | ||||||
|   , 'Date: ' + (new Date()).toUTCString() |  | ||||||
|   , 'Connection: close' |  | ||||||
|   , 'Content-Type: text/html' |  | ||||||
|   , 'Content-Length: 9' |  | ||||||
|   , '' |  | ||||||
|   , 'Not Found' |  | ||||||
|   ].join('\r\n')); |  | ||||||
| } |  | ||||||
| function rejectNonWebsocket(req, res) { |  | ||||||
|   // status code 426 = Upgrade Required
 |  | ||||||
|   res.statusCode = 426; |  | ||||||
|   res.setHeader('Content-Type', 'application/json'); |  | ||||||
|   res.send({error: { message: 'Only websockets accepted for tunnel server' }}); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var defaultConfig = { |  | ||||||
|   servernames: [] |  | ||||||
| , secret: null |  | ||||||
| }; |  | ||||||
| var tunnelFuncs = { |  | ||||||
|   // These functions should not be called because connections to the admin domains
 |  | ||||||
|   // should already be decrypted, and connections to non-client domains should never
 |  | ||||||
|   // be given to us in the first place.
 |  | ||||||
|   httpsTunnel:  httpsTunnel |  | ||||||
| , httpsInvalid: httpsTunnel |  | ||||||
|   // These function should not be called because ACME challenges should be handled
 |  | ||||||
|   // before admin domain connections are given to us, and the only non-encrypted
 |  | ||||||
|   // client connections that should be given to us are ACME challenges.
 |  | ||||||
| , handleHttp:         handleHttp |  | ||||||
| , handleInsecureHttp: handleHttp |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config) { |  | ||||||
|   var equal = require('deep-equal'); |  | ||||||
|   var enableDestroy = require('server-destroy'); |  | ||||||
|   var currentOpts = Object.assign({}, defaultConfig); |  | ||||||
| 
 |  | ||||||
|   var httpServer, wsServer, stunneld; |  | ||||||
|   function start() { |  | ||||||
|     if (httpServer || wsServer || stunneld) { |  | ||||||
|       throw new Error('trying to start already started tunnel server'); |  | ||||||
|     } |  | ||||||
|     httpServer = require('http').createServer(rejectNonWebsocket); |  | ||||||
|     enableDestroy(httpServer); |  | ||||||
| 
 |  | ||||||
|     wsServer = new (require('ws').Server)({ server: httpServer }); |  | ||||||
| 
 |  | ||||||
|     var tunnelOpts = Object.assign({}, tunnelFuncs, currentOpts); |  | ||||||
|     stunneld = require('stunneld').create(tunnelOpts); |  | ||||||
|     wsServer.on('connection', stunneld.ws); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function stop() { |  | ||||||
|     if (!httpServer || !wsServer || !stunneld) { |  | ||||||
|       throw new Error('trying to stop unstarted tunnel server (or it got into semi-initialized state'); |  | ||||||
|     } |  | ||||||
|     wsServer.close(); |  | ||||||
|     wsServer = null; |  | ||||||
|     httpServer.destroy(); |  | ||||||
|     httpServer = null; |  | ||||||
|     // Nothing to close here, just need to set it to null to allow it to be garbage-collected.
 |  | ||||||
|     stunneld = null; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function updateConf() { |  | ||||||
|     var newOpts = Object.assign({}, defaultConfig, config.tunnelServer); |  | ||||||
|     if (!Array.isArray(newOpts.servernames)) { |  | ||||||
|       newOpts.servernames = []; |  | ||||||
|     } |  | ||||||
|     var trimmedOpts = { |  | ||||||
|       servernames: newOpts.servernames.slice().sort() |  | ||||||
|     , secret:      newOpts.secret |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     if (equal(trimmedOpts, currentOpts)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     currentOpts = trimmedOpts; |  | ||||||
| 
 |  | ||||||
|     // Stop what's currently running, then if we are still supposed to be running then we
 |  | ||||||
|     // can start it again with the updated options. It might be possible to make use of
 |  | ||||||
|     // the existing http and ws servers when the config changes, but I'm not sure what
 |  | ||||||
|     // state the actions needed to close all existing connections would put them in.
 |  | ||||||
|     if (httpServer || wsServer || stunneld) { |  | ||||||
|       stop(); |  | ||||||
|     } |  | ||||||
|     if (currentOpts.servernames.length && currentOpts.secret) { |  | ||||||
|       start(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   process.nextTick(updateConf); |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     isAdminDomain: function (domain) { |  | ||||||
|       return currentOpts.servernames.indexOf(domain) !== -1; |  | ||||||
|     } |  | ||||||
|   , handleAdminConn: function (conn) { |  | ||||||
|       if (!httpServer) { |  | ||||||
|         console.error(new Error('handleAdminConn called with no active tunnel server')); |  | ||||||
|         conn.end(); |  | ||||||
|       } else { |  | ||||||
|         return httpServer.emit('connection', conn); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|   , isClientDomain: function (domain) { |  | ||||||
|       if (!stunneld) { return false; } |  | ||||||
|       return stunneld.isClientDomain(domain); |  | ||||||
|     } |  | ||||||
|   , handleClientConn: function (conn) { |  | ||||||
|       if (!stunneld) { |  | ||||||
|         console.error(new Error('handleClientConn called with no active tunnel server')); |  | ||||||
|         conn.end(); |  | ||||||
|       } else { |  | ||||||
|         return stunneld.tcp(conn); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|   , updateConf |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										144
									
								
								lib/tunnel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								lib/tunnel.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (opts, servers) { | ||||||
|  |   // servers = { plainserver, server }
 | ||||||
|  |   var Oauth3 = require('oauth3-cli'); | ||||||
|  |   var Tunnel = require('daplie-tunnel').create({ | ||||||
|  |     Oauth3: Oauth3 | ||||||
|  |   , PromiseA: opts.PromiseA | ||||||
|  |   , CLI: { | ||||||
|  |       init: function (rs, ws/*, state, options*/) { | ||||||
|  |         // noop
 | ||||||
|  |         return ws; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }).Tunnel; | ||||||
|  |   var stunnel = require('stunnel'); | ||||||
|  |   var killcount = 0; | ||||||
|  | 
 | ||||||
|  |   /* | ||||||
|  |   var Dup = { | ||||||
|  |     write: function (chunk, encoding, cb) { | ||||||
|  |       this.__my_socket.push(chunk, encoding); | ||||||
|  |       cb(); | ||||||
|  |     } | ||||||
|  |   , read: function (size) { | ||||||
|  |       var x = this.__my_socket.read(size); | ||||||
|  |       if (x) { this.push(x); } | ||||||
|  |     } | ||||||
|  |   , setTimeout: function () { | ||||||
|  |       console.log('TODO implement setTimeout on Duplex'); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   var httpServer = require('http').createServer(function (req, res) { | ||||||
|  |     console.log('req.socket.encrypted', req.socket.encrypted); | ||||||
|  |     res.end('Hello, tunneled World!'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   var tlsServer = require('tls').createServer(opts.httpsOptions, function (tlsSocket) { | ||||||
|  |     console.log('tls connection'); | ||||||
|  |     // things get a little messed up here
 | ||||||
|  |     httpServer.emit('connection', tlsSocket); | ||||||
|  | 
 | ||||||
|  |     // try again
 | ||||||
|  |     //servers.server.emit('connection', tlsSocket);
 | ||||||
|  |   }); | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  |   process.on('SIGINT', function () { | ||||||
|  |     killcount += 1; | ||||||
|  |     console.log('[quit] closing http and https servers'); | ||||||
|  |     if (killcount >= 3) { | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |     if (servers.server) { | ||||||
|  |       servers.server.close(); | ||||||
|  |     } | ||||||
|  |     if (servers.insecureServer) { | ||||||
|  |       servers.insecureServer.close(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return Tunnel.token({ | ||||||
|  |     refreshToken: opts.refreshToken | ||||||
|  |   , email: opts.email | ||||||
|  |   , domains: opts.sites.map(function (site) { | ||||||
|  |       return site.name; | ||||||
|  |     }) | ||||||
|  |   , device: { hostname: opts.devicename || opts.device } | ||||||
|  |   }).then(function (result) { | ||||||
|  |     // { jwt, tunnelUrl }
 | ||||||
|  |     var locals = []; | ||||||
|  |     opts.sites.map(function (site) { | ||||||
|  |       locals.push({ | ||||||
|  |         protocol: 'https' | ||||||
|  |       , hostname: site.name | ||||||
|  |       , port: opts.port | ||||||
|  |       }); | ||||||
|  |       locals.push({ | ||||||
|  |         protocol: 'http' | ||||||
|  |       , hostname: site.name | ||||||
|  |       , port: opts.insecurePort || opts.port | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     return stunnel.connect({ | ||||||
|  |       token: result.jwt | ||||||
|  |     , stunneld: result.tunnelUrl | ||||||
|  |       // XXX TODO BUG // this is just for testing
 | ||||||
|  |     , insecure: /*opts.insecure*/ true | ||||||
|  |     , locals: locals | ||||||
|  |       // a simple passthru is proving to not be so simple
 | ||||||
|  |     , net: require('net') /* | ||||||
|  |       { | ||||||
|  |         createConnection: function (info, cb) { | ||||||
|  |           // data is the hello packet / first chunk
 | ||||||
|  |           // info = { data, servername, port, host, remoteAddress: { family, address, port } }
 | ||||||
|  | 
 | ||||||
|  |           var myDuplex = new (require('stream').Duplex)(); | ||||||
|  |           var myDuplex2 = new (require('stream').Duplex)(); | ||||||
|  |           // duplex = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
 | ||||||
|  | 
 | ||||||
|  |           myDuplex2.__my_socket = myDuplex; | ||||||
|  |           myDuplex.__my_socket = myDuplex2; | ||||||
|  | 
 | ||||||
|  |           myDuplex2._write = Dup.write; | ||||||
|  |           myDuplex2._read = Dup.read; | ||||||
|  | 
 | ||||||
|  |           myDuplex._write = Dup.write; | ||||||
|  |           myDuplex._read = Dup.read; | ||||||
|  | 
 | ||||||
|  |           myDuplex.remoteFamily = info.remoteFamily; | ||||||
|  |           myDuplex.remoteAddress = info.remoteAddress; | ||||||
|  |           myDuplex.remotePort = info.remotePort; | ||||||
|  | 
 | ||||||
|  |           // socket.local{Family,Address,Port}
 | ||||||
|  |           myDuplex.localFamily = 'IPv4'; | ||||||
|  |           myDuplex.localAddress = '127.0.01'; | ||||||
|  |           myDuplex.localPort = info.port; | ||||||
|  | 
 | ||||||
|  |           myDuplex.setTimeout = Dup.setTimeout; | ||||||
|  | 
 | ||||||
|  |           // this doesn't seem to work so well
 | ||||||
|  |           //servers.server.emit('connection', myDuplex);
 | ||||||
|  | 
 | ||||||
|  |           // try a little more manual wrapping / unwrapping
 | ||||||
|  |           var firstByte = info.data[0]; | ||||||
|  |           if (firstByte < 32 || firstByte >= 127) { | ||||||
|  |             tlsServer.emit('connection', myDuplex); | ||||||
|  |           } | ||||||
|  |           else { | ||||||
|  |             httpServer.emit('connection', myDuplex); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (cb) { | ||||||
|  |             process.nextTick(cb); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return myDuplex2; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       //*/
 | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										57
									
								
								lib/udp.js
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								lib/udp.js
									
									
									
									
									
								
							| @ -1,57 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (deps, config) { |  | ||||||
|   var listeners = require('./servers').listeners.udp; |  | ||||||
| 
 |  | ||||||
|   function packetHandler(port, msg) { |  | ||||||
|     if (!Array.isArray(config.udp.modules)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var socket = require('dgram').createSocket('udp4'); |  | ||||||
|     config.udp.modules.forEach(function (mod) { |  | ||||||
|       if (mod.type !== 'forward') { |  | ||||||
|         // To avoid logging bad modules every time we get a UDP packet we assign a warned
 |  | ||||||
|         // property to the module (non-enumerable so it won't be saved to the config or
 |  | ||||||
|         // show up in the API).
 |  | ||||||
|         if (!mod.warned) { |  | ||||||
|           console.warn('found bad DNS module', mod); |  | ||||||
|           Object.defineProperty(mod, 'warned', {value: true, enumerable: false}); |  | ||||||
|         } |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (mod.ports.indexOf(port) < 0) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       var dest = require('./domain-utils').separatePort(mod.address || ''); |  | ||||||
|       dest.port = dest.port || mod.port; |  | ||||||
|       dest.host = dest.host || mod.host || 'localhost'; |  | ||||||
|       socket.send(msg, dest.port, dest.host); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function updateListeners() { |  | ||||||
|     var current = listeners.list(); |  | ||||||
|     var wanted = config.udp.bind; |  | ||||||
| 
 |  | ||||||
|     if (!Array.isArray(wanted)) { wanted = []; } |  | ||||||
|     wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356); |  | ||||||
| 
 |  | ||||||
|     current.forEach(function (port) { |  | ||||||
|       if (wanted.indexOf(port) < 0) { |  | ||||||
|         listeners.close(port); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     wanted.forEach(function (port) { |  | ||||||
|       if (current.indexOf(port) < 0) { |  | ||||||
|         listeners.add(port, packetHandler.bind(port)); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   updateListeners(); |  | ||||||
|   return { |  | ||||||
|     updateConf: updateListeners |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,64 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var config; |  | ||||||
| var modules; |  | ||||||
| 
 |  | ||||||
| // Everything that uses the config should be reading it when relevant rather than
 |  | ||||||
| // just at the beginning, so we keep the reference for the main object and just
 |  | ||||||
| // change all of its properties to match the new config.
 |  | ||||||
| function update(conf) { |  | ||||||
|   var newKeys = Object.keys(conf); |  | ||||||
| 
 |  | ||||||
|   Object.keys(config).forEach(function (key) { |  | ||||||
|     if (newKeys.indexOf(key) < 0) { |  | ||||||
|       delete config[key]; |  | ||||||
|     } else { |  | ||||||
|       config[key] = conf[key]; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   console.log('config update', JSON.stringify(config)); |  | ||||||
|   Object.values(modules).forEach(function (mod) { |  | ||||||
|     if (typeof mod.updateConf === 'function') { |  | ||||||
|       mod.updateConf(config); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function create(conf) { |  | ||||||
|   var PromiseA = require('bluebird'); |  | ||||||
|   var OAUTH3 = require('../packages/assets/org.oauth3'); |  | ||||||
|   require('../packages/assets/org.oauth3/oauth3.domains.js'); |  | ||||||
|   require('../packages/assets/org.oauth3/oauth3.dns.js'); |  | ||||||
|   require('../packages/assets/org.oauth3/oauth3.tunnel.js'); |  | ||||||
|   OAUTH3._hooks = require('../packages/assets/org.oauth3/oauth3.node.storage.js'); |  | ||||||
| 
 |  | ||||||
|   config = conf; |  | ||||||
|   var deps = { |  | ||||||
|     messenger: process |  | ||||||
|   , PromiseA: PromiseA |  | ||||||
|   , OAUTH3: OAUTH3 |  | ||||||
|   , request: PromiseA.promisify(require('request')) |  | ||||||
|   , recase: require('recase').create({}) |  | ||||||
|     // Note that if a custom createConnections is used it will be called with different
 |  | ||||||
|     // sets of custom options based on what is actually being proxied. Most notably the
 |  | ||||||
|     // HTTP proxying connection creation is not something we currently control.
 |  | ||||||
|   , net: require('net') |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   modules = { |  | ||||||
|     storage:  require('./storage').create(deps, conf) |  | ||||||
|   , socks5:   require('./socks5-server').create(deps, conf) |  | ||||||
|   , ddns:     require('./ddns').create(deps, conf) |  | ||||||
|   , mdns:     require('./mdns').create(deps, conf) |  | ||||||
|   , udp:      require('./udp').create(deps, conf) |  | ||||||
|   , tcp:      require('./tcp').create(deps, conf) |  | ||||||
|   , stunneld: require('./tunnel-server-manager').create(deps, config) |  | ||||||
|   }; |  | ||||||
|   Object.assign(deps, modules); |  | ||||||
| 
 |  | ||||||
|   process.removeListener('message', create); |  | ||||||
|   process.on('message', update); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| process.on('message', create); |  | ||||||
							
								
								
									
										58
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								package.json
									
									
									
									
									
								
							| @ -1,14 +1,14 @@ | |||||||
| { | { | ||||||
|   "name": "goldilocks", |   "name": "goldilocks", | ||||||
|   "version": "1.1.6", |   "version": "2.2.0", | ||||||
|   "description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.", |   "description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.", | ||||||
|   "main": "bin/goldilocks.js", |   "main": "bin/goldilocks.js", | ||||||
|   "repository": { |   "repository": { | ||||||
|     "type": "git", |     "type": "git", | ||||||
|     "url": "git.coolaj86.com:coolaj86/goldilocks.js.git" |     "url": "git@git.daplie.com:Daplie/goldilocks.js.git" | ||||||
|   }, |   }, | ||||||
|   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", |   "author": "AJ ONeal <aj@daplie.com> (https://daplie.com/)", | ||||||
|   "license": "(MIT OR Apache-2.0)", |   "license": "SEE LICENSE IN LICENSE.txt", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "test": "node bin/goldilocks.js -p 8443 -d /tmp/" |     "test": "node bin/goldilocks.js -p 8443 -d /tmp/" | ||||||
|   }, |   }, | ||||||
| @ -34,42 +34,34 @@ | |||||||
|     "server" |     "server" | ||||||
|   ], |   ], | ||||||
|   "bugs": { |   "bugs": { | ||||||
|     "url": "https://git.coolaj86.com/coolaj86/goldilocks.js/issues" |     "url": "https://git.daplie.com/Daplie/server-https/issues" | ||||||
|   }, |   }, | ||||||
|   "homepage": "https://git.coolaj86.com/coolaj86/goldilocks.js", |   "homepage": "https://git.daplie.com/Daplie/goldilocks.js#readme", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "bluebird": "^3.4.6", |     "bluebird": "^3.4.6", | ||||||
|     "body-parser": "1", |     "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", | ||||||
|     "commander": "^2.9.0", |     "daplie-tunnel": "git+https://git.daplie.com/Daplie/daplie-cli-tunnel.git#master", | ||||||
|     "deep-equal": "^1.0.1", |     "ddns-cli": "git+https://git.daplie.com/Daplie/node-ddns-client.git#master", | ||||||
|     "dns-suite": "1", |     "express": "git+https://github.com/expressjs/express.git#4.x", | ||||||
|     "express": "4", |  | ||||||
|     "finalhandler": "^0.4.0", |     "finalhandler": "^0.4.0", | ||||||
|     "greenlock": "2.1", |     "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", | ||||||
|     "http-proxy": "^1.16.2", |     "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master", | ||||||
|     "human-readable-ids": "1", |     "httpolyglot": "^0.1.1", | ||||||
|     "ipaddr.js": "v1.3", |     "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", | ||||||
|     "js-yaml": "^3.8.3", |     "ipify": "^1.1.0", | ||||||
|     "jsonschema": "^1.2.0", |     "js-yaml": "^3.8.1", | ||||||
|     "jsonwebtoken": "^7.4.0", |     "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", | ||||||
|     "le-challenge-fs": "2", |     "le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master", | ||||||
|     "le-challenge-sni": "^2.0.1", |     "le-challenge-sni": "^2.0.1", | ||||||
|     "le-store-certbot": "2", |     "livereload": "^0.6.0", | ||||||
|     "localhost.daplie.me-certificates": "^1.3.5", |     "localhost.daplie.me-certificates": "^1.3.0", | ||||||
|     "network": "^0.4.0", |     "minimist": "^1.1.1", | ||||||
|     "recase": "v1.0.4", |     "oauth3-cli": "git+https://git.daplie.com/OAuth3/oauth3-cli.git#master", | ||||||
|  |     "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", |     "scmp": "git+https://github.com/freewil/scmp.git#1.x", | ||||||
|     "scmp": "1", |  | ||||||
|     "serve-index": "^1.7.0", |     "serve-index": "^1.7.0", | ||||||
|     "serve-static": "^1.10.0", |     "serve-static": "^1.10.0", | ||||||
|     "server-destroy": "^1.0.1", |     "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" | ||||||
|     "sni": "^1.0.0", |  | ||||||
|     "socket-pair": "^1.0.3", |  | ||||||
|     "socksv5": "0.0.6", |  | ||||||
|     "stunnel": "1.0", |  | ||||||
|     "stunneld": "0.9", |  | ||||||
|     "tunnel-packer": "^1.3.0", |  | ||||||
|     "ws": "^2.3.1" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										179
									
								
								packages/apis/com.daplie.caddy/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								packages/apis/com.daplie.caddy/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,179 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.dependencies = [ 'OAUTH3', 'storage.owners', 'options.device' ]; | ||||||
|  | module.exports.create = function (deps) { | ||||||
|  |   var scmp = require('scmp'); | ||||||
|  |   var crypto = require('crypto'); | ||||||
|  |   var jwt = require('jsonwebtoken'); | ||||||
|  |   var bodyParser = require('body-parser'); | ||||||
|  |   var jsonParser = bodyParser.json({ | ||||||
|  |     inflate: true, limit: '100kb', reviver: null, strict: true /* type, verify */ | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   var api = deps.api; | ||||||
|  | 
 | ||||||
|  |   /* | ||||||
|  |   var owners; | ||||||
|  |   deps.storage.owners.on('set', function (_owners) { | ||||||
|  |     owners = _owners; | ||||||
|  |   }); | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  |   function isAuthorized(req, res, fn) { | ||||||
|  |     var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, '')); | ||||||
|  |     if (!auth) { | ||||||
|  |       res.setHeader('Content-Type', 'application/json;'); | ||||||
|  |       res.end(JSON.stringify({ error: { message: "no token", code: 'E_NO_TOKEN', uri: undefined } })); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var id = crypto.createHash('sha256').update(auth.sub).digest('hex'); | ||||||
|  |     return deps.storage.owners.exists(id).then(function (exists) { | ||||||
|  |       if (!exists) { | ||||||
|  |         res.setHeader('Content-Type', 'application/json;'); | ||||||
|  |         res.end(JSON.stringify({ error: { message: "not authorized", code: 'E_NO_AUTHZ', uri: undefined } })); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       req.userId = id; | ||||||
|  |       fn(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     init: function (req, res) { | ||||||
|  |       jsonParser(req, res, function () { | ||||||
|  | 
 | ||||||
|  |       return deps.PromiseA.resolve().then(function () { | ||||||
|  | 
 | ||||||
|  |         console.log('req.body', req.body); | ||||||
|  |         var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, '')); | ||||||
|  |         var token = jwt.decode(req.body.access_token); | ||||||
|  |         var refresh = jwt.decode(req.body.refresh_token); | ||||||
|  |         auth.sub = auth.sub || auth.acx.id; | ||||||
|  |         token.sub = token.sub || token.acx.id; | ||||||
|  |         refresh.sub = refresh.sub || refresh.acx.id; | ||||||
|  | 
 | ||||||
|  |         // TODO validate token with issuer, but as-is the sub is already a secret
 | ||||||
|  |         var id = crypto.createHash('sha256').update(auth.sub).digest('hex'); | ||||||
|  |         var tid = crypto.createHash('sha256').update(token.sub).digest('hex'); | ||||||
|  |         var rid = crypto.createHash('sha256').update(refresh.sub).digest('hex'); | ||||||
|  |         var session = { | ||||||
|  |           access_token: req.body.access_token | ||||||
|  |         , token: token | ||||||
|  |         , refresh_token: req.body.refresh_token | ||||||
|  |         , refresh: refresh | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         console.log('ids', id, tid, rid); | ||||||
|  | 
 | ||||||
|  |         if (req.body.ip_url) { | ||||||
|  |           // TODO set options / GunDB
 | ||||||
|  |           deps.options.ip_url = req.body.ip_url; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return deps.storage.owners.all().then(function (results) { | ||||||
|  |           console.log('results', results); | ||||||
|  |           var err; | ||||||
|  | 
 | ||||||
|  |           // There is no owner yet. First come, first serve.
 | ||||||
|  |           if (!results || !results.length) { | ||||||
|  |             if (tid !== id || rid !== id) { | ||||||
|  |               err = new Error( | ||||||
|  |                 "When creating an owner the Authorization Bearer and Token and Refresh must all match" | ||||||
|  |               ); | ||||||
|  |               return deps.PromiseA.reject(err); | ||||||
|  |             } | ||||||
|  |             console.log('no owner, creating'); | ||||||
|  |             return deps.storage.owners.set(id, session); | ||||||
|  |           } | ||||||
|  |           console.log('has results'); | ||||||
|  | 
 | ||||||
|  |           // There are onwers. Is this one of them?
 | ||||||
|  |           if (!results.some(function (token) { | ||||||
|  |             return scmp(id, token.id); | ||||||
|  |           })) { | ||||||
|  |             err = new Error("Authorization token does not belong to an existing owner."); | ||||||
|  |             return deps.PromiseA.reject(err); | ||||||
|  |           } | ||||||
|  |           console.log('has correct owner'); | ||||||
|  | 
 | ||||||
|  |           // We're adding an owner, unless it already exists
 | ||||||
|  |           if (!results.some(function (token) { | ||||||
|  |             return scmp(tid, token.id); | ||||||
|  |           })) { | ||||||
|  |             console.log('adds new owner with existing owner'); | ||||||
|  |             return deps.storage.owners.set(id, session); | ||||||
|  |           } | ||||||
|  |         }).then(function () { | ||||||
|  |           res.setHeader('Content-Type', 'application/json;'); | ||||||
|  |           res.end(JSON.stringify({ success: true })); | ||||||
|  |         }); | ||||||
|  |       }, function (err) { | ||||||
|  |         res.setHeader('Content-Type', 'application/json;'); | ||||||
|  |         res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   , tunnel: function (req, res) { | ||||||
|  |       isAuthorized(req, res, function () { | ||||||
|  |         jsonParser(req, res, function () { | ||||||
|  | 
 | ||||||
|  |           console.log('req.body', req.body); | ||||||
|  | 
 | ||||||
|  |           return deps.storage.owners.get(req.userId).then(function (session) { | ||||||
|  |             session.token.id = req.userId; | ||||||
|  |             return api.tunnel(deps, session); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   , config: function (req, res) { | ||||||
|  |       isAuthorized(req, res, function () { | ||||||
|  |         if ('POST' !== req.method) { | ||||||
|  |           res.setHeader('Content-Type', 'application/json;'); | ||||||
|  |           res.end(JSON.stringify(deps.recase.snakeCopy(deps.options))); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         jsonParser(req, res, function () { | ||||||
|  | 
 | ||||||
|  |           console.log('req.body', req.body); | ||||||
|  | 
 | ||||||
|  |           deps.storage.config.merge(req.body); | ||||||
|  |           deps.storage.config.save(); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   , request: function (req, res) { | ||||||
|  |       jsonParser(req, res, function () { | ||||||
|  |       isAuthorized(req, res, function () { | ||||||
|  | 
 | ||||||
|  |         deps.request({ | ||||||
|  |           method: req.body.method || 'GET' | ||||||
|  |         , url: req.body.url | ||||||
|  |         , headers: req.body.headers | ||||||
|  |         , body: req.body.data | ||||||
|  |         }).then(function (resp) { | ||||||
|  |           if (resp.body instanceof Buffer || 'string' === typeof resp.body) { | ||||||
|  |             resp.body = JSON.parse(resp.body); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return { | ||||||
|  |             statusCode: resp.statusCode | ||||||
|  |           , status: resp.status | ||||||
|  |           , headers: resp.headers | ||||||
|  |           , body: resp.body | ||||||
|  |           , data: resp.data | ||||||
|  |           }; | ||||||
|  |         }).then(function (result) { | ||||||
|  |           res.send(result); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |       }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   , _api: api | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										23
									
								
								packages/apis/com.daplie.caddy/test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/apis/com.daplie.caddy/test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | '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') | ||||||
|  | ); | ||||||
							
								
								
									
										1
									
								
								packages/assets/org.oauth3
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										1
									
								
								packages/assets/org.oauth3
									
									
									
									
									
										Submodule
									
								
							| @ -0,0 +1 @@ | |||||||
|  | Subproject commit 3a805d071a4a84371b9bc674839d2511dd9aa4d3 | ||||||
							
								
								
									
										23
									
								
								stages/01-serve.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								stages/01-serve.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var https = require('httpolyglot'); | ||||||
|  | var httpsOptions = require('localhost.daplie.me-certificates').merge({}); | ||||||
|  | var httpsPort = 8443; | ||||||
|  | var redirectApp = require('redirect-https')({ | ||||||
|  |   port: httpsPort | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | var server = https.createServer(httpsOptions); | ||||||
|  | 
 | ||||||
|  | server.on('request', function (req, res) { | ||||||
|  |   if (!req.socket.encrypted) { | ||||||
|  |     redirectApp(req, res); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   res.end("Hello, Encrypted World!"); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | server.listen(httpsPort, function () { | ||||||
|  |   console.log('https://' + 'localhost.daplie.me' + (443 === httpsPort ? ':' : ':' + httpsPort)); | ||||||
|  | }); | ||||||
							
								
								
									
										3
									
								
								terms.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								terms.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | # adding TOS to TXT DNS Record | ||||||
|  | daplie dns:set -n _terms._cloud.localhost.foo.daplie.me -t TXT -a '{"url":"oauth3.org/tos/draft","explicit":true}' --ttl 3600 | ||||||
|  | daplie dns:set -n _terms._cloud.localhost.alpha.daplie.me -t TXT -a '{"url":"oauth3.org/tos/draft","explicit":true}' --ttl 3600 | ||||||
							
								
								
									
										17
									
								
								test-chain.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								test-chain.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | 
 | ||||||
|  | node serve.js \ | ||||||
|  |   --port 8443 \ | ||||||
|  |   --key node_modules/localhost.daplie.me-certificates/privkey.pem \ | ||||||
|  |   --cert node_modules/localhost.daplie.me-certificates/fullchain.pem \ | ||||||
|  |   --root node_modules/localhost.daplie.me-certificates/root.pem \ | ||||||
|  |   -c "$(cat node_modules/localhost.daplie.me-certificates/root.pem)" & | ||||||
|  | 
 | ||||||
|  | PID=$! | ||||||
|  | 
 | ||||||
|  | sleep 1 | ||||||
|  | curl -s --insecure http://localhost.daplie.me:8443 > ./root.pem | ||||||
|  | curl -s https://localhost.daplie.me:8443 --cacert ./root.pem | ||||||
|  | 
 | ||||||
|  | rm ./root.pem | ||||||
|  | kill $PID 2>/dev/null | ||||||
							
								
								
									
										22
									
								
								update-packages.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								update-packages.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | pushd packages/assets | ||||||
|  | 
 | ||||||
|  | git clone https://git.daplie.com/Daplie/oauth3.js.git org.oauth3 | ||||||
|  | pushd org.oauth3 | ||||||
|  | git checkout master | ||||||
|  | git pull | ||||||
|  | popd | ||||||
|  | 
 | ||||||
|  | mkdir -p com.jquery | ||||||
|  | pushd com.jquery | ||||||
|  | wget 'https://code.jquery.com/jquery-3.1.1.js' -O jquery-3.1.1.js | ||||||
|  | popd | ||||||
|  | 
 | ||||||
|  | mkdir -p com.google | ||||||
|  | pushd com.google | ||||||
|  | wget 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js' -O angular.1.6.2.min.js | ||||||
|  | popd | ||||||
|  | 
 | ||||||
|  | mkdir -p well-known | ||||||
|  | pushd well-known | ||||||
|  | ln -sf ../org.oauth3/well-known/oauth3 ./oauth3 | ||||||
|  | popd | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user