Compare commits
	
		
			No commits in common. "master" and "tunnel" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,7 +1,6 @@ | ||||
| *session* | ||||
| *secret* | ||||
| var/* | ||||
| packages/assets/org.oauth3 | ||||
| 
 | ||||
| # 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 | ||||
| ========== | ||||
| 
 | ||||
| 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 | ||||
| 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 | ||||
| goldilocks | ||||
| ``` | ||||
| @ -73,581 +48,114 @@ goldilocks | ||||
| Serving /Users/foo/ at https://localhost.daplie.me:8443 | ||||
| ``` | ||||
| 
 | ||||
| Install as a System Service (daemon-mode) | ||||
| 
 | ||||
| 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 | ||||
| Usage | ||||
| ----- | ||||
| 
 | ||||
| Goldilocks has several core systems, which all have their own configuration and | ||||
| 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: | ||||
| Examples: | ||||
| 
 | ||||
| ``` | ||||
| root        The path to serve as a string. | ||||
|             The template variable `:hostname` represents the HTTP Host header without port information | ||||
|             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 | ||||
| # Install | ||||
| npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js | ||||
| 
 | ||||
| index       Set to `false` to disable the default behavior of loading `index.html` in directories | ||||
|             ex: `false` | ||||
| # Use tunnel | ||||
| goldilocks --sites jane.daplie.me --agree-tos --email jane@example.com --tunnel | ||||
| 
 | ||||
| dotfiles    Set to `allow` to load dotfiles rather than ignoring them | ||||
|             ex: `"allow"` | ||||
| 
 | ||||
| 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 | ||||
| # BEFORE you access in a browser for the first time, use curl | ||||
| # (because there's a concurrency bug in the greenlock setup) | ||||
| curl https://jane.daplie.me | ||||
| ``` | ||||
| 
 | ||||
| Example config: | ||||
| ```yml | ||||
| http: | ||||
|   modules: | ||||
|     - type: static | ||||
|       domains: | ||||
|         - example.com | ||||
|       root: /srv/www/:hostname | ||||
| Options: | ||||
| 
 | ||||
| * `-p <port>` - i.e. `sudo goldilocks -p 443` (defaults to 80+443 or 8443) | ||||
| * `-d <dirpath>` - i.e. `goldilocks -d /tmp/` (defaults to `pwd`) | ||||
|   * you can use `:hostname` as a template for multiple directories | ||||
|   * Example A: `goldilocks -d /srv/www/:hostname --sites localhost.foo.daplie.me,localhost.bar.daplie.me` | ||||
|   * Example B: `goldilocks -d ./:hostname/public/ --sites localhost.foo.daplie.me,localhost.bar.daplie.me` | ||||
| * `-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: | ||||
| ``` | ||||
| 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: | ||||
| Examples | ||||
| -------- | ||||
| 
 | ||||
| ```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. | ||||
| ``` | ||||
| Host example.com | ||||
|   ProxyCommand openssl s_client -quiet -connect example.com:443 -servername ssh.example.com | ||||
| ``` | ||||
| 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). | ||||
| 
 | ||||
| #### 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 | ||||
| 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 | ||||
| 
 | ||||
| ``` | ||||
| [openvpn-over-goldilocks] | ||||
| client = yes | ||||
| accept = 127.0.0.1:1194 | ||||
| sni = vpn.example.com | ||||
| connect = example.com:443 | ||||
| ```bash | ||||
| goldilocks -p 8443 \ | ||||
|   --sites test.mooo.com | ||||
|   --key /etc/letsencrypt/live/test.mooo.com/privkey.pem \ | ||||
|   --cert /etc/letsencrypt/live/test.mooo.com/fullchain.pem \ | ||||
|   --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 | ||||
| 
 | ||||
| The forward module routes traffic based on port number **without decrypting** it. | ||||
| 
 | ||||
| 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 | ||||
| ```bash | ||||
| curl --insecure https://test.mooo.com:8443 > ./root.pem | ||||
| curl https://test.mooo.com:8843 --cacert ./root.pem | ||||
| ``` | ||||
| 
 | ||||
| Example Config: | ||||
| ```yml | ||||
| 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 | ||||
| * [QuickStart Guide for Let's Encrypt](https://coolaj86.com/articles/lets-encrypt-on-raspberry-pi/) | ||||
| * [QuickStart Guide for FreeDNS](https://coolaj86.com/articles/free-dns-hosting-with-freedns-afraid-org.html) | ||||
|  | ||||
| @ -11,12 +11,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | ||||
|       return Oauth3.PromiseA.resolve(session); | ||||
|     }; | ||||
|     var auth = Oauth3.create(); | ||||
|     auth.setProvider('oauth3.org').then(function () { | ||||
|       auth.checkSession().then(function (session) { | ||||
|         console.log('hasSession?', session); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     auth.setProvider('oauth3.org'); | ||||
|     window.oauth3 = auth; // debug
 | ||||
|     return auth; | ||||
|   } ]) | ||||
| @ -144,13 +139,8 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | ||||
| 
 | ||||
|     vm.authenticate = function () { | ||||
|       // 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); | ||||
| 
 | ||||
|         return oauth3.api('domains.list').then(function (domains) { | ||||
| @ -161,7 +151,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | ||||
| 
 | ||||
|             return OAUTH3.request({ | ||||
|               method: 'POST' | ||||
|             , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/init' | ||||
|             , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/init' | ||||
|             , session: session | ||||
|             , data: { | ||||
|                 access_token: session.access_token | ||||
| @ -185,7 +175,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | ||||
|               console.info('Initialized Goldilocks', resp); | ||||
|               return OAUTH3.request({ | ||||
|                 method: 'GET' | ||||
|               , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/config' | ||||
|               , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/config' | ||||
|               , session: session | ||||
|               }).then(function (configResp) { | ||||
|                 console.log('config', configResp.data); | ||||
| @ -223,7 +213,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | ||||
|                 vm.admin.network.iface = 'gateway'; | ||||
|                 return OAUTH3.request({ | ||||
|                   method: 'POST' | ||||
|                 , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/request' | ||||
|                 , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/request' | ||||
|                 , session: session | ||||
|                 , data: { | ||||
|                     method: 'GET' | ||||
| @ -250,15 +240,24 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | ||||
|     }; | ||||
| 
 | ||||
|     vm.enableTunnel = function (/*opts*/) { | ||||
|       vm.admin.network.iface = 'oauth3-tunnel'; | ||||
| 
 | ||||
|       return oauth3.request({ | ||||
|         method: 'POST' | ||||
|       , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/tunnel' | ||||
|       }).then(function (result) { | ||||
|         // vm.admin.network.iface = 'oauth3-tunnel';
 | ||||
|         return result; | ||||
|       , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/tunnel' | ||||
|       /* | ||||
|       , data: { | ||||
|           method: 'GET' | ||||
|         , url: 'https://api.ipify.org?format=json' | ||||
|         } | ||||
|       */ | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     oauth3.checkSession().then(function (session) { | ||||
|       console.log('hasSession?', session); | ||||
|     }); | ||||
| 
 | ||||
|     /* | ||||
|     console.log('OAUTH3.PromiseA', OAUTH3.PromiseA); | ||||
|     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", | ||||
|   "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.", | ||||
|   "main": "bin/goldilocks.js", | ||||
|   "repository": { | ||||
|     "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/)", | ||||
|   "license": "(MIT OR Apache-2.0)", | ||||
|   "author": "AJ ONeal <aj@daplie.com> (https://daplie.com/)", | ||||
|   "license": "SEE LICENSE IN LICENSE.txt", | ||||
|   "scripts": { | ||||
|     "test": "node bin/goldilocks.js -p 8443 -d /tmp/" | ||||
|   }, | ||||
| @ -34,42 +34,34 @@ | ||||
|     "server" | ||||
|   ], | ||||
|   "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": { | ||||
|     "bluebird": "^3.4.6", | ||||
|     "body-parser": "1", | ||||
|     "commander": "^2.9.0", | ||||
|     "deep-equal": "^1.0.1", | ||||
|     "dns-suite": "1", | ||||
|     "express": "4", | ||||
|     "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", | ||||
|     "daplie-tunnel": "git+https://git.daplie.com/Daplie/daplie-cli-tunnel.git#master", | ||||
|     "ddns-cli": "git+https://git.daplie.com/Daplie/node-ddns-client.git#master", | ||||
|     "express": "git+https://github.com/expressjs/express.git#4.x", | ||||
|     "finalhandler": "^0.4.0", | ||||
|     "greenlock": "2.1", | ||||
|     "http-proxy": "^1.16.2", | ||||
|     "human-readable-ids": "1", | ||||
|     "ipaddr.js": "v1.3", | ||||
|     "js-yaml": "^3.8.3", | ||||
|     "jsonschema": "^1.2.0", | ||||
|     "jsonwebtoken": "^7.4.0", | ||||
|     "le-challenge-fs": "2", | ||||
|     "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", | ||||
|     "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master", | ||||
|     "httpolyglot": "^0.1.1", | ||||
|     "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", | ||||
|     "ipify": "^1.1.0", | ||||
|     "js-yaml": "^3.8.1", | ||||
|     "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", | ||||
|     "le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master", | ||||
|     "le-challenge-sni": "^2.0.1", | ||||
|     "le-store-certbot": "2", | ||||
|     "localhost.daplie.me-certificates": "^1.3.5", | ||||
|     "network": "^0.4.0", | ||||
|     "recase": "v1.0.4", | ||||
|     "livereload": "^0.6.0", | ||||
|     "localhost.daplie.me-certificates": "^1.3.0", | ||||
|     "minimist": "^1.1.1", | ||||
|     "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", | ||||
|     "request": "^2.81.0", | ||||
|     "scmp": "1", | ||||
|     "scmp": "git+https://github.com/freewil/scmp.git#1.x", | ||||
|     "serve-index": "^1.7.0", | ||||
|     "serve-static": "^1.10.0", | ||||
|     "server-destroy": "^1.0.1", | ||||
|     "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" | ||||
|     "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										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