forked from coolaj86/goldilocks.js
		
	Compare commits
	
		
			339 Commits
		
	
	
		
			serve-http
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | dccebfe16b | ||
|  | a87e69e332 | ||
| 8fb910ddf9 | |||
| 158892f88c | |||
| e462978154 | |||
| 3a7e4cd2ab | |||
| 4f16f92208 | |||
|  | 34dff39358 | ||
|  | 136431d493 | ||
|  | 4b9e07842d | ||
|  | 43105ba266 | ||
|  | add6745475 | ||
|  | 2969eb3247 | ||
|  | 2c6e5cfa46 | ||
|  | 037c4df6e0 | ||
|  | dd7bc74dad | ||
|  | 12c2fd1819 | ||
|  | a8aedcbc31 | ||
|  | ea010427e8 | ||
|  | d8cc8fe8e6 | ||
|  | 11f2d37044 | ||
|  | 40bd1d9cc6 | ||
|  | 2277b22d9d | ||
|  | 11809030c6 | ||
|  | b6b9d5f2f3 | ||
|  | b307a2bcf2 | ||
|  | 0a233cfcf0 | ||
|  | 4ffad8d3c3 | ||
|  | 0e1437bcd7 | ||
|  | a17f7d52ba | ||
|  | dd035219a3 | ||
|  | 57f97eebdb | ||
|  | ce31c2c02d | ||
|  | 4baf475e35 | ||
|  | 0611645ef0 | ||
|  | 0024d51289 | ||
|  | 62b4c79236 | ||
|  | fbdf0e8a28 | ||
|  | 1382b8b4e2 | ||
|  | 828712bf12 | ||
|  | ccf45ab06e | ||
|  | ac36a35c19 | ||
|  | a2d81e4302 | ||
|  | 6ae1e463c9 | ||
|  | 8ee24fcd77 | ||
|  | 8c34316979 | ||
|  | 011559b1a4 | ||
| 65920f8fce | |||
| 32f2f707cc | |||
| 75d2680830 | |||
| a2d1797d0f | |||
| 0b464cab36 | |||
| 07920b594c | |||
| 0935e3e4b3 | |||
| 35016cd124 | |||
|  | cec4f1ee95 | ||
|  | 4b2e6b1600 | ||
|  | 352b1b0a4a | ||
|  | c40a17dceb | ||
|  | 186a68a0ad | ||
|  | e071b8c3eb | ||
|  | fe477300aa | ||
|  | 278ba38398 | ||
|  | 041138f4b2 | ||
|  | 3bb6dc9680 | ||
|  | 5c7a5c0b2e | ||
|  | 55f81ca1b6 | ||
|  | ecf5f038dd | ||
|  | 307d81690d | ||
|  | 2f06c7fbdc | ||
|  | b332b1fc89 | ||
|  | 33c54149c0 | ||
|  | 669587a07e | ||
|  | 64fc41377f | ||
|  | 680cb05f89 | ||
|  | 847824f97a | ||
|  | 11715f1405 | ||
|  | e0fe188846 | ||
|  | 34ce5ed4ee | ||
|  | e3c99636c5 | ||
|  | 28f28c6eb9 | ||
|  | ef5dcb81f4 | ||
|  | b4e967f152 | ||
|  | 5de8edb33d | ||
|  | b1d5ed3b14 | ||
|  | b324016056 | ||
|  | eda766e48c | ||
|  | a27252eb77 | ||
|  | 7423d6065f | ||
|  | 9ec642237c | ||
|  | 16589e65f6 | ||
|  | 9a63f30bf2 | ||
|  | c697008573 | ||
|  | c132861cab | ||
|  | 4a576da545 | ||
|  | af14149a13 | ||
|  | c637671c78 | ||
|  | 5534ba2ef1 | ||
|  | b44ad7b17a | ||
|  | 138f59bea3 | ||
|  | 0ef845f2d5 | ||
|  | e504c4b717 | ||
|  | de3977d1e4 | ||
|  | c9318b65b0 | ||
|  | 20cf66c67d | ||
|  | 72ff65e833 | ||
|  | c4af0d05ec | ||
|  | 019ec2b990 | ||
|  | 5e48a2ed5e | ||
|  | 85472588c3 | ||
|  | 00de23ded7 | ||
|  | 82f0b45c56 | ||
|  | acf2fd7764 | ||
|  | c23f5ae25b | ||
|  | 019e4fa063 | ||
|  | 3aed276faf | ||
|  | b9fac21b05 | ||
|  | c55c034f11 | ||
|  | 6b2b91ba26 | ||
|  | cfaa8d4959 | ||
|  | 9c7aaa4f98 | ||
|  | f2ce3e9fe1 | ||
|  | 754ace5cb4 | ||
|  | 72520679d8 | ||
|  | e15d4f830e | ||
|  | 5e9e2662e0 | ||
|  | 663fdba446 | ||
|  | 0406d0cd93 | ||
|  | 503da9efd0 | ||
|  | 2a57a1e12c | ||
|  | 79ef9694b7 | ||
|  | 61af4707ee | ||
|  | ea55d3cc73 | ||
|  | 8371170a14 | ||
|  | 485a223f86 | ||
|  | bd3292bbf2 | ||
|  | 5761ab9d62 | ||
|  | 8f4a733391 | ||
|  | ded53cf45c | ||
|  | 0380a8087f | ||
|  | d04b750f87 | ||
|  | cc6b34dd46 | ||
|  | 12e4a47855 | ||
|  | f25a0191bd | ||
|  | 3d3fac5087 | ||
|  | b8f282db79 | ||
|  | 9e9b5ca9ad | ||
|  | 0dd20e4dfc | ||
|  | 5cc7e3f187 | ||
|  | 83f72730a2 | ||
|  | 8930a528bc | ||
|  | cfcc1acb8c | ||
|  | a625ee9db9 | ||
|  | 528e58969e | ||
|  | 68d6322b42 | ||
|  | fcb2de516f | ||
|  | bc301b94c9 | ||
|  | 44d11e094b | ||
|  | c47b1dc235 | ||
|  | e5a12db270 | ||
|  | e02ecc86d9 | ||
|  | 42adabdb20 | ||
|  | b65697ea74 | ||
|  | 66e9ecd2bf | ||
|  | fee0df3ec9 | ||
|  | 188869b83e | ||
|  | 983a6e2cd7 | ||
|  | 2357319194 | ||
|  | 7863b9cee6 | ||
|  | 3bd9bac390 | ||
|  | 363620d7fb | ||
|  | d84299356b | ||
|  | e3de5f76be | ||
|  | d859d0a44f | ||
|  | 49474fd413 | ||
|  | 388ce522ae | ||
|  | 26e015f5e3 | ||
|  | 7ee247afe6 | ||
|  | 267ff2486a | ||
|  | d9b20b5aeb | ||
|  | c34b0444c1 | ||
|  | f3beb4795f | ||
|  | 6ba0cac3f3 | ||
|  | 95d5526f28 | ||
|  | b5a99c4e9b | ||
|  | b361c0cd53 | ||
|  | 2ffd846352 | ||
|  | 1957dd8d80 | ||
|  | 10fc80c2b7 | ||
|  | 59c9abca49 | ||
|  | e52ae83aa4 | ||
|  | 85a0c3d421 | ||
|  | 0daf1b909a | ||
|  | e62869b661 | ||
|  | a4aad3184a | ||
|  | f37730c97d | ||
|  | 000d36e76a | ||
|  | caa7b343d4 | ||
|  | 2b70001309 | ||
|  | 4a6d21f0b5 | ||
|  | e901f1679b | ||
|  | aea4725fb0 | ||
|  | 403ec90c2d | ||
|  | 3ac0f3077e | ||
|  | 7a2f0f0984 | ||
|  | 0c71b83fa5 | ||
|  | fb288bfdbc | ||
|  | 0a0f06094e | ||
|  | 72ff8ebf15 | ||
|  | 7408db6601 | ||
|  | 8fb70564db | ||
|  | 49d5e5296a | ||
|  | 61018d9303 | ||
|  | 30777af804 | ||
|  | a216178ee0 | ||
|  | cb3f43c7ca | ||
|  | 651e53daf1 | ||
|  | 4d49e0fb63 | ||
|  | 78c1fb344e | ||
|  | e96ebfc1fc | ||
|  | d12c06999e | ||
|  | cad8dd686e | ||
|  | f569391cd9 | ||
|  | 78da05b630 | ||
|  | ec07b6fcdb | ||
|  | 027494cd1d | ||
|  | 50cee61ac6 | ||
|  | 1c811ac444 | ||
|  | 90a683e03d | ||
|  | 3293dcea56 | ||
|  | 231e54d808 | ||
|  | d5dee498f5 | ||
|  | dda3dffb17 | ||
|  | be1a60d2e7 | ||
|  | 810d0a8e90 | ||
|  | 69d7d9e4b8 | ||
|  | d4573994fc | ||
|  | 8e2e071abf | ||
|  | d9486b8297 | ||
|  | be6900cd50 | ||
|  | e259c4d0ce | ||
|  | 509f2f4f4f | ||
|  | 112034e26c | ||
|  | 5c7f2321cc | ||
|  | 002c0059eb | ||
|  | bd1ca9f584 | ||
|  | 2eb6d1bc95 | ||
|  | 3633c7570b | ||
|  | 21a77ad10a | ||
|  | be67f04afa | ||
|  | 1e3021c669 | ||
|  | 1f8e44947f | ||
|  | 7c115c33aa | ||
|  | 6a7273907b | ||
|  | 73d3396609 | ||
|  | 78e8266ce3 | ||
|  | 100e7cee7c | ||
|  | 5bbf57a57a | ||
|  | aa28a72f3f | ||
|  | dbbae2311c | ||
|  | 27e818f41a | ||
|  | 47bcdcf2a6 | ||
|  | df3a818914 | ||
|  | d25ceadf4a | ||
|  | e386b19e3f | ||
|  | febe106a81 | ||
|  | 15c80dab14 | ||
|  | 1731d09849 | ||
|  | 474f9766d8 | ||
|  | d16f857fca | ||
|  | 0047ae69f4 | ||
|  | 3aa1085008 | ||
|  | 47d72365cc | ||
|  | b229bbc6cb | ||
|  | 8599d383df | ||
|  | 5719a8a434 | ||
|  | 87de2c65ad | ||
|  | 5777a885a4 | ||
|  | e24f9412dd | ||
|  | 158c363c88 | ||
|  | 70e7d57395 | ||
|  | afca49feae | ||
|  | 56113cb100 | ||
|  | bcba0abddc | ||
|  | ab011d1829 | ||
|  | ab31bae6ff | ||
|  | b3b407d161 | ||
|  | b1c1aba7a5 | ||
|  | 569b2c49c2 | ||
|  | 0b877f9c9c | ||
|  | b14c90501b | ||
|  | c7924ca164 | ||
|  | e70da5af22 | ||
|  | b57b18f5ed | ||
|  | 5af64078ce | ||
|  | ea3506c352 | ||
|  | 388733568d | ||
|  | 0187114160 | ||
|  | 99a3de6496 | ||
|  | f32db19b52 | ||
|  | 953bdda67e | ||
|  | dad2e66f52 | ||
|  | 513e6e8bdd | ||
|  | 1bdcd73d28 | ||
|  | 5451cbb4da | ||
|  | ed9ed3d21b | ||
|  | 3a96004038 | ||
|  | 5f97e1bd67 | ||
|  | 569e3b02d2 | ||
|  | d71240a222 | ||
|  | c75a073ce4 | ||
|  | f6ef5bcad8 | ||
|  | 9546c489cb | ||
|  | 7d7a2c2f0d | ||
|  | 3e1abaddf4 | ||
|  | 8c4594f399 | ||
|  | 2414163179 | ||
|  | 9ee2d7b890 | ||
|  | 5bf95b8b25 | ||
|  | b2cc88698b | ||
|  | 0a0f37f85c | ||
|  | 41d36a4eb9 | ||
|  | 9cf9604c00 | ||
|  | 3c5bc9103e | ||
|  | c1a98b2db3 | ||
|  | 0a7e70517f | ||
|  | f4de15b14f | ||
|  | dbd1e23bfa | ||
|  | aed520a653 | ||
|  | eacf2e0dbf | ||
|  | f2b05ee7af | ||
|  | c7627faffd | ||
|  | 0fdd2773b5 | ||
|  | 350d87c38d | ||
|  | 4b470ffe51 | ||
|  | 58a0b592ff | ||
|  | dc55169415 | ||
|  | 67aa28aece | ||
|  | 4267955286 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,7 @@ | |||||||
| *session* | *session* | ||||||
| *secret* | *secret* | ||||||
| var/* | var/* | ||||||
|  | packages/assets/org.oauth3 | ||||||
| 
 | 
 | ||||||
| # Logs | # Logs | ||||||
| logs | logs | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +0,0 @@ | |||||||
| [submodule "packages/assets/org.oauth3"] |  | ||||||
| 	path = packages/assets/org.oauth3 |  | ||||||
| 	url = git@git.daplie.com:OAuth3/oauth3.js.git |  | ||||||
| @ -13,4 +13,5 @@ | |||||||
| , "latedef": true | , "latedef": true | ||||||
| , "curly": true | , "curly": true | ||||||
| , "trailing": true | , "trailing": true | ||||||
|  | , "esversion": 6 | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										171
									
								
								API.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								API.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,171 @@ | |||||||
|  | # 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								CHANGELOG
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | v1.1.5 - Implemented dns-01 ACME challenges | ||||||
|  | 
 | ||||||
|  | v1.1.4 - Improved responsiveness to config updates | ||||||
|  |   * changed which TCP/UDP ports are bound to on config update | ||||||
|  |   * update tunnel server settings on config update | ||||||
|  |   * update socks5 setting on config update | ||||||
|  | 
 | ||||||
|  | v1.1.3 - Better late than never... here's some stuff we've got | ||||||
|  |   * fixed (probably) network settings not being readable | ||||||
|  |   * supports timeouts in loopback check | ||||||
|  |   * loopback check less likely to fail / throw errors, will try again | ||||||
|  |   * supports ddns using audience of token | ||||||
							
								
								
									
										41
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | Copyright 2017 Daplie, Inc | ||||||
|  | 
 | ||||||
|  | This is open source software; you can redistribute it and/or modify it under the | ||||||
|  | terms of either: | ||||||
|  | 
 | ||||||
|  |    a) the "MIT License" | ||||||
|  |    b) the "Apache-2.0 License" | ||||||
|  | 
 | ||||||
|  | MIT License | ||||||
|  | 
 | ||||||
|  |    Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  |    of this software and associated documentation files (the "Software"), to deal | ||||||
|  |    in the Software without restriction, including without limitation the rights | ||||||
|  |    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  |    copies of the Software, and to permit persons to whom the Software is | ||||||
|  |    furnished to do so, subject to the following conditions: | ||||||
|  | 
 | ||||||
|  |    The above copyright notice and this permission notice shall be included in all | ||||||
|  |    copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  |    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  |    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  |    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  |    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  |    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  |    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  |    SOFTWARE. | ||||||
|  | 
 | ||||||
|  | Apache-2.0 License Summary | ||||||
|  | 
 | ||||||
|  |    Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |    you may not use this file except in compliance with the License. | ||||||
|  |    You may obtain a copy of the License at | ||||||
|  | 
 | ||||||
|  |      http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | 
 | ||||||
|  |    Unless required by applicable law or agreed to in writing, software | ||||||
|  |    distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |    See the License for the specific language governing permissions and | ||||||
|  |    limitations under the License. | ||||||
| @ -1,3 +0,0 @@ | |||||||
| Hello all. We make our source code available to view, but we retain copyright. |  | ||||||
| 
 |  | ||||||
| It's not because we're trying to be mean or anything, we just want to maintain our distribution channel. |  | ||||||
							
								
								
									
										716
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										716
									
								
								README.md
									
									
									
									
									
								
							| @ -1,45 +1,70 @@ | |||||||
| <!-- BANNER_TPL_BEGIN --> |  | ||||||
| 
 |  | ||||||
| About Daplie: We're taking back the Internet! |  | ||||||
| -------------- |  | ||||||
| 
 |  | ||||||
| Down with Google, Apple, and Facebook! |  | ||||||
| 
 |  | ||||||
| We're re-decentralizing the web and making it read-write again - one home cloud system at a time. |  | ||||||
| 
 |  | ||||||
| Tired of serving the Empire? Come join the Rebel Alliance: |  | ||||||
| 
 |  | ||||||
| <a href="mailto:jobs@daplie.com">jobs@daplie.com</a> | [Invest in Daplie on Wefunder](https://daplie.com/invest/) | [Pre-order Cloud](https://daplie.com/preorder/), The World's First Home Server for Everyone |  | ||||||
| 
 |  | ||||||
| <!-- BANNER_TPL_END --> |  | ||||||
| 
 |  | ||||||
| Goldilocks | Goldilocks | ||||||
| ========== | ========== | ||||||
| 
 | 
 | ||||||
| The node.js webserver that's just right. | The node.js netserver 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 | ||||||
| 
 | 
 | ||||||
| A simple HTTPS static file server with valid TLS (SSL) certs. | Install Standalone | ||||||
| 
 |  | ||||||
| Comes bundled a valid certificate for localhost.daplie.me, |  | ||||||
| which is great for testing and development, and you can specify your own. |  | ||||||
| 
 |  | ||||||
| Also great for testing ACME certs from letsencrypt.org. |  | ||||||
| 
 |  | ||||||
| Install |  | ||||||
| ------- | ------- | ||||||
| 
 | 
 | ||||||
|  | ### curl | bash | ||||||
|  | 
 | ||||||
| ```bash | ```bash | ||||||
| # v2 in npm | curl -fsSL https://git.coolaj86.com/coolaj86/goldilocks.js/raw/v1.1/installer/get.sh | bash | ||||||
| npm install -g goldilocks |  | ||||||
| 
 |  | ||||||
| # master in git (via ssh) |  | ||||||
| npm install -g git+ssh://git@git.daplie.com:Daplie/goldilocks.js |  | ||||||
| 
 |  | ||||||
| # master in git (unauthenticated) |  | ||||||
| npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ### git | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | git clone https://git.coolaj86.com/coolaj86/goldilocks.js | ||||||
|  | pushd goldilocks.js | ||||||
|  | git checkout v1.1 | ||||||
|  | bash installer/install.sh | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### npm | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | # v1 in git (unauthenticated) | ||||||
|  | npm install -g git+https://git@git.coolaj86.com:coolaj86/goldilocks.js#v1 | ||||||
|  | 
 | ||||||
|  | # v1 in git (via ssh) | ||||||
|  | npm install -g git+ssh://git@git.coolaj86.com:coolaj86/goldilocks.js#v1 | ||||||
|  | 
 | ||||||
|  | # v1 in npm | ||||||
|  | npm install -g goldilocks@v1 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Uninstall | ||||||
|  | 
 | ||||||
|  | Remove goldilocks and services: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | rm -rf /opt/goldilocks/ /srv/goldilocks/ /var/goldilocks/ /var/log/goldilocks/ /etc/tmpfiles.d/goldilocks.conf /etc/systemd/system/goldilocks.service | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Remove config as well | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | rm -rf /etc/goldilocks/ /etc/ssl/goldilocks | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Usage | ||||||
|  | ----- | ||||||
|  | 
 | ||||||
| ```bash | ```bash | ||||||
| goldilocks | goldilocks | ||||||
| ``` | ``` | ||||||
| @ -48,114 +73,581 @@ goldilocks | |||||||
| Serving /Users/foo/ at https://localhost.daplie.me:8443 | Serving /Users/foo/ at https://localhost.daplie.me:8443 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Usage | 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 | ||||||
| ----- | ----- | ||||||
| 
 | 
 | ||||||
| Examples: | Goldilocks has several core systems, which all have their own configuration and | ||||||
|  | some of which have modules: | ||||||
| 
 | 
 | ||||||
| ``` | * [http](#http) | ||||||
| # Install |   - [proxy (reverse proxy)](#httpproxy-how-to-reverse-proxy-ruby-python-etc) | ||||||
| npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js |   - [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 | ||||||
| 
 | 
 | ||||||
| # Use tunnel | All modules require a `type` and an `id`, and any modules not defined inside the | ||||||
| goldilocks --sites jane.daplie.me --agree-tos --email jane@example.com --tunnel | `domains` system also require a `domains` field (with the exception of the `forward` | ||||||
|  | modules that require the `ports` field). | ||||||
| 
 | 
 | ||||||
| # BEFORE you access in a browser for the first time, use curl | ### http | ||||||
| # (because there's a concurrency bug in the greenlock setup) | 
 | ||||||
| curl https://jane.daplie.me | 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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Options: | ### http.proxy - how to reverse proxy (ruby, python, etc) | ||||||
| 
 | 
 | ||||||
| * `-p <port>` - i.e. `sudo goldilocks -p 443` (defaults to 80+443 or 8443) | The proxy module is for reverse proxying, typically to an application on the same machine. | ||||||
| * `-d <dirpath>` - i.e. `goldilocks -d /tmp/` (defaults to `pwd`) | (Though it can also reverse proxy to other devices on the local network.) | ||||||
|   * 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: |  | ||||||
| 
 | 
 | ||||||
|  | It has the following options: | ||||||
| ``` | ``` | ||||||
| --root /path/to/primary-root.pem --root /path/to/cross-root.pem | 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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Other options: | 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 | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
| * `--serve-root true` alias for `-c` with the contents of root.pem | ### http.static - how to serve a web page | ||||||
| * `--sites example.com` changes the servername logged to the console |  | ||||||
| * `--letsencrypt-certs example.com` sets and key, fullchain, and root to standard letsencrypt locations |  | ||||||
| 
 | 
 | ||||||
| Examples | The static module is for serving static web pages and assets and has the following options: | ||||||
| -------- | 
 | ||||||
|  | ``` | ||||||
|  | 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 | ||||||
|  | 
 | ||||||
|  | index       Set to `false` to disable the default behavior of loading `index.html` in directories | ||||||
|  |             ex: `false` | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Example config: | ||||||
|  | ```yml | ||||||
|  | http: | ||||||
|  |   modules: | ||||||
|  |     - type: static | ||||||
|  |       domains: | ||||||
|  |         - example.com | ||||||
|  |       root: /srv/www/:hostname | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### http.redirect - how to redirect URLs | ||||||
|  | 
 | ||||||
|  | The redirect module is for, you guessed it, redirecting URLs. | ||||||
|  | 
 | ||||||
|  | It has the following options: | ||||||
|  | ``` | ||||||
|  | status      The HTTP status code to issue (301 is usual permanent redirect, 302 is temporary) | ||||||
|  |             ex: 301 | ||||||
|  | 
 | ||||||
|  | from        The URL path that was used in the request. | ||||||
|  |             The `*` wildcard character can be used for matching a full segment of the path | ||||||
|  |             ex: /photos/ | ||||||
|  |             ex: /photos/*/*/ | ||||||
|  | 
 | ||||||
|  | to          The new URL path which should be used. | ||||||
|  |             If wildcards matches were used they will be available as `:1`, `:2`, etc. | ||||||
|  |             ex: /pics/ | ||||||
|  |             ex: /pics/:1/:2/ | ||||||
|  |             ex: https://mydomain.com/photos/:1/:2/ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Example config: | ||||||
|  | ```yml | ||||||
|  | http: | ||||||
|  |   modules: | ||||||
|  |     - type: proxy | ||||||
|  |       domains: | ||||||
|  |         - example.com | ||||||
|  |       status: 301 | ||||||
|  |       from: /archives/*/*/*/ | ||||||
|  |       to: https://example.net/year/:1/month/:2/day/:3/ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### tls | ||||||
|  | 
 | ||||||
|  | The tls system handles encrypted connections, including fetching certificates, | ||||||
|  | and uses ServerName Indication (SNI) to determine if the connection should be | ||||||
|  | handled by the http system, a tls system module, or rejected. | ||||||
|  | 
 | ||||||
|  | Example config: | ||||||
|  | ```yml | ||||||
|  | tls: | ||||||
|  |   modules: | ||||||
|  |     - type: proxy | ||||||
|  |       domains: | ||||||
|  |         - example.com | ||||||
|  |         - example.net | ||||||
|  |       address: '127.0.0.1:6443' | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Certificates are saved to `~/acme`, which may be `/var/www/acme` if Goldilocks is run as the www-data user. | ||||||
|  | 
 | ||||||
|  | ### tls.proxy | ||||||
|  | 
 | ||||||
|  | The proxy module routes the traffic based on the ServerName Indication (SNI) **without decrypting** it. | ||||||
|  | 
 | ||||||
|  | It has the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc). | ||||||
|  | 
 | ||||||
|  | Example config: | ||||||
|  | ```yml | ||||||
|  | tls: | ||||||
|  |   modules: | ||||||
|  |     - type: proxy | ||||||
|  |       domains: | ||||||
|  |         - example.com | ||||||
|  |       address: '127.0.0.1:5443' | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### tls.acme | ||||||
|  | 
 | ||||||
|  | The acme module defines the setting used when getting new certificates. | ||||||
|  | 
 | ||||||
|  | It has the following options: | ||||||
|  | ``` | ||||||
|  | email              The email address for ACME certificate issuance | ||||||
|  |                    ex: john.doe@example.com | ||||||
|  | 
 | ||||||
|  | server             The ACME server to use | ||||||
|  |                    ex: https://acme-v01.api.letsencrypt.org/directory | ||||||
|  |                    ex: https://acme-staging.api.letsencrypt.org/directory | ||||||
|  | 
 | ||||||
|  | challenge_type     The ACME challenge to request | ||||||
|  |                    ex: http-01, dns-01, tls-01 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Example config: | ||||||
|  | ```yml | ||||||
|  | tls: | ||||||
|  |   modules: | ||||||
|  |     - type: acme | ||||||
|  |       domains: | ||||||
|  |         - example.com | ||||||
|  |         - example.net | ||||||
|  |       email: 'joe.shmoe@example.com' | ||||||
|  |       server: 'https://acme-staging.api.letsencrypt.org/directory' | ||||||
|  |       challenge_type: 'http-01' | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | **NOTE:** If you specify `dns-01` as the challenge type there must also be a | ||||||
|  | [DDNS module](#ddns) defined for all of the relevant domains (though not all | ||||||
|  | domains handled by a single TLS module need to be handled by the same DDNS | ||||||
|  | module). The DDNS module provides all of the information needed to actually | ||||||
|  | set the DNS records needed to verify ownership. | ||||||
|  | 
 | ||||||
|  | ### tcp | ||||||
|  | 
 | ||||||
|  | The tcp system handles both *raw* and *tls-terminated* tcp network traffic | ||||||
|  | (see the _Note_ section below the example). It may use port numbers | ||||||
|  | or traffic sniffing to determine how the connection should be handled. | ||||||
|  | 
 | ||||||
|  | It has the following options: | ||||||
|  | ``` | ||||||
|  | bind      An array of numeric ports on which to bind | ||||||
|  |           ex: 80 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Example Config: | ||||||
|  | ```yml | ||||||
|  | tcp: | ||||||
|  |   bind: | ||||||
|  |     - 22 | ||||||
|  |     - 80 | ||||||
|  |     - 443 | ||||||
|  |   modules: | ||||||
|  |     - type: forward | ||||||
|  |       ports: | ||||||
|  |         - 22 | ||||||
|  |       address: '127.0.0.1:2222' | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | _Note_: When tcp traffic comes into goldilocks it will be tested against the tcp modules. | ||||||
|  | The connection may be handed to the TLS module if it appears to be a TLS/SSL/HTTPS connection | ||||||
|  | and if the tls module terminates the traffic, the connection will be sent back to the TLS module. | ||||||
|  | Due to the complexity of node.js' networking stack it is not currently possible to tell which | ||||||
|  | port tls-terminated traffic came from, so only the SNI header (serername / domain name) may be used for | ||||||
|  | modules matching terminated TLS. | ||||||
|  | 
 | ||||||
|  | ### tcp.proxy | ||||||
|  | 
 | ||||||
|  | The proxy module routes traffic **after tls-termination** based on the servername (domain name) | ||||||
|  | contained in a SNI header. As such this only works to route TCP connections wrapped in a TLS stream. | ||||||
|  | 
 | ||||||
|  | It has the same options as the [HTTP proxy module](#httpproxy-how-to-reverse-proxy-ruby-python-etc). | ||||||
|  | 
 | ||||||
|  | This is particularly useful for routing ssh and vpn traffic over tcp port 443 as wrapped TLS | ||||||
|  | connections in order to access one of your servers even when connecting from a harsh or potentially | ||||||
|  | misconfigured network environment (i.e. hotspots in public libraries and shopping malls). | ||||||
|  | 
 | ||||||
|  | Example config: | ||||||
|  | ```yml | ||||||
|  | tcp: | ||||||
|  |   modules: | ||||||
|  |     - type: proxy | ||||||
|  |       domains: | ||||||
|  |         - ssh.example.com      # Note: this domain would also listed in tls.acme.domains | ||||||
|  |       host: localhost | ||||||
|  |       port: 22 | ||||||
|  |     - type: proxy | ||||||
|  |       domains: | ||||||
|  |         - vpn.example.com      # Note: this domain would also listed in tls.acme.domains | ||||||
|  |       host: localhost | ||||||
|  |       port: 1194 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | _Note_: In same cases network administrators purposefully block ssh and vpn connections using | ||||||
|  | Application Firewalls with DPI (deep packet inspection) enabled. You should read the ToS of the | ||||||
|  | network you are connected to to ensure that you aren't subverting policies that are purposefully | ||||||
|  | in place on such networks. | ||||||
|  | 
 | ||||||
|  | #### Using with ssh | ||||||
|  | 
 | ||||||
|  | In order to use this to route SSH connections you will need to use `ssh`'s | ||||||
|  | `ProxyCommand` option. For example to use the TLS certificate for `ssh.example.com` | ||||||
|  | to wrap an ssh connection you could use the following command: | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| goldilocks -p 1443 -c 'Hello from 1443' & | ssh user@example.com -o ProxyCommand='openssl s_client -quiet -connect example.com:443 -servername ssh.example.com' | ||||||
| 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] |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| And if you tested <http://localhost.daplie.me:3443> in a browser, | Alternatively you could add the following lines to your ssh config file. | ||||||
| it would redirect to <https://localhost.daplie.me:3443> (on the same port). | ``` | ||||||
|  | Host example.com | ||||||
|  |   ProxyCommand openssl s_client -quiet -connect example.com:443 -servername ssh.example.com | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
| (in curl it would just show an error message) | #### Using with OpenVPN | ||||||
| 
 | 
 | ||||||
| ### Testing ACME Let's Encrypt certs | There are two strategies that will work well for you: | ||||||
| 
 | 
 | ||||||
| In case you didn't know, you can get free https certificates from | 1) [Use ssh](https://redfern.me/tunneling-openvpn-through-ssh/) with the config above to reverse proxy tcp port 1194 to you. | ||||||
| [letsencrypt.org](https://letsencrypt.org) |  | ||||||
| (ACME letsencrypt) |  | ||||||
| and even a free subdomain from <https://freedns.afraid.org>. |  | ||||||
| 
 |  | ||||||
| If you want to quickly test the certificates you installed, |  | ||||||
| you can do so like this: |  | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| goldilocks -p 8443 \ | ssh -L 1194:localhost:1194 example.com | ||||||
|   --letsencrypt-certs test.mooo.com \ |  | ||||||
|   --serve-root true |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| which is equilavent to | 2) [Use stunnel]https://serverfault.com/questions/675553/stunnel-vpn-traffic-and-ensure-it-looks-like-ssl-traffic-on-port-443/681497) | ||||||
| 
 | 
 | ||||||
| ```bash | ``` | ||||||
| goldilocks -p 8443 \ | [openvpn-over-goldilocks] | ||||||
|   --sites test.mooo.com | client = yes | ||||||
|   --key /etc/letsencrypt/live/test.mooo.com/privkey.pem \ | accept = 127.0.0.1:1194 | ||||||
|   --cert /etc/letsencrypt/live/test.mooo.com/fullchain.pem \ | sni = vpn.example.com | ||||||
|   --root /etc/letsencrypt/live/test.mooo.com/root.pem \ | connect = example.com:443 | ||||||
|   -c "$(cat 'sudo /etc/letsencrypt/live/test.mooo.com/root.pem')" |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| and can be tested like so | 3) [Use stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js) as described in the "tunnel_server" section below. | ||||||
| 
 | 
 | ||||||
| ```bash | ### tcp.forward | ||||||
| curl --insecure https://test.mooo.com:8443 > ./root.pem | 
 | ||||||
| curl https://test.mooo.com:8843 --cacert ./root.pem | 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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| * [QuickStart Guide for Let's Encrypt](https://coolaj86.com/articles/lets-encrypt-on-raspberry-pi/) | Example Config: | ||||||
| * [QuickStart Guide for FreeDNS](https://coolaj86.com/articles/free-dns-hosting-with-freedns-afraid-org.html) | ```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 | ||||||
|  | |||||||
| @ -11,7 +11,12 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|       return Oauth3.PromiseA.resolve(session); |       return Oauth3.PromiseA.resolve(session); | ||||||
|     }; |     }; | ||||||
|     var auth = Oauth3.create(); |     var auth = Oauth3.create(); | ||||||
|     auth.setProvider('oauth3.org'); |     auth.setProvider('oauth3.org').then(function () { | ||||||
|  |       auth.checkSession().then(function (session) { | ||||||
|  |         console.log('hasSession?', session); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     window.oauth3 = auth; // debug
 |     window.oauth3 = auth; // debug
 | ||||||
|     return auth; |     return auth; | ||||||
|   } ]) |   } ]) | ||||||
| @ -139,8 +144,13 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
| 
 | 
 | ||||||
|     vm.authenticate = function () { |     vm.authenticate = function () { | ||||||
|       // TODO authorization redirect /api/org.oauth3.consumer/authorization_redirect/:provider_uri
 |       // TODO authorization redirect /api/org.oauth3.consumer/authorization_redirect/:provider_uri
 | ||||||
|  |       var opts = { | ||||||
|  |         type: 'popup' | ||||||
|  |       , scope: 'domains,dns' | ||||||
|  |       // , debug: true
 | ||||||
|  |       }; | ||||||
| 
 | 
 | ||||||
|       return oauth3.authenticate().then(function (session) { |       return oauth3.authenticate(opts).then(function (session) { | ||||||
|         console.info("Authorized Session", session); |         console.info("Authorized Session", session); | ||||||
| 
 | 
 | ||||||
|         return oauth3.api('domains.list').then(function (domains) { |         return oauth3.api('domains.list').then(function (domains) { | ||||||
| @ -151,7 +161,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
| 
 | 
 | ||||||
|             return OAUTH3.request({ |             return OAUTH3.request({ | ||||||
|               method: 'POST' |               method: 'POST' | ||||||
|             , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/init' |             , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/init' | ||||||
|             , session: session |             , session: session | ||||||
|             , data: { |             , data: { | ||||||
|                 access_token: session.access_token |                 access_token: session.access_token | ||||||
| @ -175,7 +185,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|               console.info('Initialized Goldilocks', resp); |               console.info('Initialized Goldilocks', resp); | ||||||
|               return OAUTH3.request({ |               return OAUTH3.request({ | ||||||
|                 method: 'GET' |                 method: 'GET' | ||||||
|               , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/config' |               , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/config' | ||||||
|               , session: session |               , session: session | ||||||
|               }).then(function (configResp) { |               }).then(function (configResp) { | ||||||
|                 console.log('config', configResp.data); |                 console.log('config', configResp.data); | ||||||
| @ -213,7 +223,7 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|                 vm.admin.network.iface = 'gateway'; |                 vm.admin.network.iface = 'gateway'; | ||||||
|                 return OAUTH3.request({ |                 return OAUTH3.request({ | ||||||
|                   method: 'POST' |                   method: 'POST' | ||||||
|                 , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/request' |                 , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/request' | ||||||
|                 , session: session |                 , session: session | ||||||
|                 , data: { |                 , data: { | ||||||
|                     method: 'GET' |                     method: 'GET' | ||||||
| @ -240,24 +250,15 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ]) | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     vm.enableTunnel = function (/*opts*/) { |     vm.enableTunnel = function (/*opts*/) { | ||||||
|       vm.admin.network.iface = 'oauth3-tunnel'; |  | ||||||
| 
 |  | ||||||
|       return oauth3.request({ |       return oauth3.request({ | ||||||
|         method: 'POST' |         method: 'POST' | ||||||
|       , url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/tunnel' |       , url: 'https://' + vm.clientUri + '/api/com.daplie.goldilocks/tunnel' | ||||||
|       /* |       }).then(function (result) { | ||||||
|       , data: { |         // vm.admin.network.iface = 'oauth3-tunnel';
 | ||||||
|           method: 'GET' |         return result; | ||||||
|         , url: 'https://api.ipify.org?format=json' |  | ||||||
|         } |  | ||||||
|       */ |  | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     oauth3.checkSession().then(function (session) { |  | ||||||
|       console.log('hasSession?', session); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     /* |     /* | ||||||
|     console.log('OAUTH3.PromiseA', OAUTH3.PromiseA); |     console.log('OAUTH3.PromiseA', OAUTH3.PromiseA); | ||||||
|     return oauth3.setProvider('oauth3.org').then(function () { |     return oauth3.setProvider('oauth3.org').then(function () { | ||||||
|  | |||||||
							
								
								
									
										1077
									
								
								bin/goldilocks.js
									
									
									
									
									
								
							
							
						
						
									
										1077
									
								
								bin/goldilocks.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										57
									
								
								dist/Library/LaunchDaemons/com.daplie.goldilocks.web.plist
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								dist/Library/LaunchDaemons/com.daplie.goldilocks.web.plist
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | <?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
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								dist/etc/goldilocks/goldilocks.example.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | |||||||
|  | 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
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								dist/etc/goldilocks/goldilocks.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
									
										69
									
								
								dist/etc/systemd/system/goldilocks.service
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								dist/etc/systemd/system/goldilocks.service
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | [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
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								dist/etc/tmpfiles.d/goldilocks.conf
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | # /etc/tmpfiles.d/goldilocks.conf | ||||||
|  | # See https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html | ||||||
|  | 
 | ||||||
|  | # Type Path           Mode UID      GID      Age Argument | ||||||
|  | d /run/goldilocks     0755 MY_USER  MY_GROUP -   - | ||||||
							
								
								
									
										20
									
								
								installer/get.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								installer/get.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | set -e | ||||||
|  | set -u | ||||||
|  | 
 | ||||||
|  | my_name=goldilocks | ||||||
|  | # TODO provide an option to supply my_ver and my_tmp | ||||||
|  | my_ver=master | ||||||
|  | my_tmp=$(mktemp -d) | ||||||
|  | 
 | ||||||
|  | mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name | ||||||
|  | git clone https://git.coolaj86.com/coolaj86/goldilocks.js.git $my_tmp/opt/$my_name/lib/node_modules/$my_name | ||||||
|  | 
 | ||||||
|  | echo "Installing to $my_tmp (will be moved after install)" | ||||||
|  | pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name | ||||||
|  |   git checkout $my_ver | ||||||
|  |   source ./installer/install.sh | ||||||
|  | popd | ||||||
|  | 
 | ||||||
|  | echo "Installation successful, now cleaning up $my_tmp ..." | ||||||
|  | rm -rf $my_tmp | ||||||
|  | echo "Done" | ||||||
							
								
								
									
										48
									
								
								installer/http-get.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								installer/http-get.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | |||||||
|  | ############################### | ||||||
|  | #                             # | ||||||
|  | #         http_get            # | ||||||
|  | # boilerplate for curl / wget # | ||||||
|  | #                             # | ||||||
|  | ############################### | ||||||
|  | 
 | ||||||
|  | # See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh | ||||||
|  | 
 | ||||||
|  | _h_http_get="" | ||||||
|  | _h_http_opts="" | ||||||
|  | _h_http_out="" | ||||||
|  | 
 | ||||||
|  | detect_http_get() | ||||||
|  | { | ||||||
|  |   set +e | ||||||
|  |   if type -p curl >/dev/null 2>&1; then | ||||||
|  |     _h_http_get="curl" | ||||||
|  |     _h_http_opts="-fsSL" | ||||||
|  |     _h_http_out="-o" | ||||||
|  |   elif type -p wget >/dev/null 2>&1; then | ||||||
|  |     _h_http_get="wget" | ||||||
|  |     _h_http_opts="--quiet" | ||||||
|  |     _h_http_out="-O" | ||||||
|  |   else | ||||||
|  |     echo "Aborted, could not find curl or wget" | ||||||
|  |     return 7 | ||||||
|  |   fi | ||||||
|  |   set -e | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | http_get() | ||||||
|  | { | ||||||
|  |   $_h_http_get $_h_http_opts $_h_http_out "$2" "$1" | ||||||
|  |   touch "$2" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | http_bash() | ||||||
|  | { | ||||||
|  |   _http_url=$1 | ||||||
|  |   #dap_args=$2 | ||||||
|  |   rm -rf dap-tmp-runner.sh | ||||||
|  |   $_h_http_get $_h_http_opts $_h_http_out dap-tmp-runner.sh "$_http_url"; bash dap-tmp-runner.sh; rm dap-tmp-runner.sh | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | detect_http_get | ||||||
|  | 
 | ||||||
|  | ## END HTTP_GET ## | ||||||
							
								
								
									
										17
									
								
								installer/install-for-launchd.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								installer/install-for-launchd.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | set -u | ||||||
|  | 
 | ||||||
|  | my_app_launchd_service="Library/LaunchDaemons/${my_app_pkg_name}.plist" | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "Installing as launchd service" | ||||||
|  | echo "" | ||||||
|  | 
 | ||||||
|  | # See http://www.launchd.info/ | ||||||
|  | safe_copy_config "$my_app_dist/$my_app_launchd_service" "$my_root/$my_app_launchd_service" | ||||||
|  | 
 | ||||||
|  | $sudo_cmd chown root:wheel "$my_root/$my_app_launchd_service" | ||||||
|  | 
 | ||||||
|  | $sudo_cmd launchctl unload -w "$my_root/$my_app_launchd_service" >/dev/null 2>/dev/null | ||||||
|  | $sudo_cmd launchctl load -w "$my_root/$my_app_launchd_service" | ||||||
|  | 
 | ||||||
|  | echo "$my_app_name started with launchd" | ||||||
							
								
								
									
										37
									
								
								installer/install-for-systemd.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								installer/install-for-systemd.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | set -u | ||||||
|  | 
 | ||||||
|  | my_app_systemd_service="etc/systemd/system/${my_app_name}.service" | ||||||
|  | my_app_systemd_tmpfiles="etc/tmpfiles.d/${my_app_name}.conf" | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "Installing as systemd service" | ||||||
|  | echo "" | ||||||
|  | 
 | ||||||
|  | sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_service" > "$my_app_dist/$my_app_systemd_service.2" | ||||||
|  | sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_service.2" > "$my_app_dist/$my_app_systemd_service" | ||||||
|  | rm "$my_app_dist/$my_app_systemd_service.2" | ||||||
|  | safe_copy_config "$my_app_dist/$my_app_systemd_service" "$my_root/$my_app_systemd_service" | ||||||
|  | $sudo_cmd chown root:root "$my_root/$my_app_systemd_service" | ||||||
|  | 
 | ||||||
|  | sed "s/MY_USER/$my_user/g" "$my_app_dist/$my_app_systemd_tmpfiles" > "$my_app_dist/$my_app_systemd_tmpfiles.2" | ||||||
|  | sed "s/MY_GROUP/$my_group/g" "$my_app_dist/$my_app_systemd_tmpfiles.2" > "$my_app_dist/$my_app_systemd_tmpfiles" | ||||||
|  | rm "$my_app_dist/$my_app_systemd_tmpfiles.2" | ||||||
|  | safe_copy_config "$my_app_dist/$my_app_systemd_tmpfiles" "$my_root/$my_app_systemd_tmpfiles" | ||||||
|  | $sudo_cmd chown root:root "$my_root/$my_app_systemd_tmpfiles" | ||||||
|  | 
 | ||||||
|  | $sudo_cmd systemctl stop "${my_app_name}.service" >/dev/null 2>/dev/null || true | ||||||
|  | $sudo_cmd systemctl daemon-reload | ||||||
|  | $sudo_cmd systemctl start "${my_app_name}.service" | ||||||
|  | $sudo_cmd systemctl enable "${my_app_name}.service" | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "" | ||||||
|  | echo "Fun systemd commands to remember:" | ||||||
|  | echo "  $sudo_cmd systemctl daemon-reload" | ||||||
|  | echo "  $sudo_cmd systemctl restart $my_app_name.service" | ||||||
|  | echo "" | ||||||
|  | echo "$my_app_name started with systemctl, check its status like so:" | ||||||
|  | echo "  $sudo_cmd systemctl status $my_app_name" | ||||||
|  | echo "  $sudo_cmd journalctl -xefu $my_app_name" | ||||||
|  | echo "" | ||||||
|  | echo "" | ||||||
							
								
								
									
										37
									
								
								installer/install-system-service.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								installer/install-system-service.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | safe_copy_config() | ||||||
|  | { | ||||||
|  |   src=$1 | ||||||
|  |   dst=$2 | ||||||
|  |   $sudo_cmd mkdir -p $(dirname "$dst") | ||||||
|  |   if [ -f "$dst" ]; then | ||||||
|  |     $sudo_cmd rsync -a "$src" "$dst.latest" | ||||||
|  |     # TODO edit config file with $my_user and $my_group | ||||||
|  |     if [ "$(cat $dst)" == "$(cat $dst.latest)" ]; then | ||||||
|  |       $sudo_cmd rm $dst.latest | ||||||
|  |     else | ||||||
|  |       echo "MANUAL INTERVENTION REQUIRED: check the systemd script update and manually decide what you want to do" | ||||||
|  |       echo "diff $dst $dst.latest" | ||||||
|  |       $sudo_cmd chown -R root:root "$dst.latest" | ||||||
|  |     fi | ||||||
|  |   else | ||||||
|  |     $sudo_cmd rsync -a --ignore-existing "$src" "$dst" | ||||||
|  |   fi | ||||||
|  |   $sudo_cmd chown -R root:root "$dst" | ||||||
|  |   $sudo_cmd chmod 644 "$dst" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | installable="" | ||||||
|  | if [ -d "$my_root/etc/systemd/system" ]; then | ||||||
|  |   source ./installer/install-for-systemd.sh | ||||||
|  |   installable="true" | ||||||
|  | fi | ||||||
|  | if [ -d "/Library/LaunchDaemons" ]; then | ||||||
|  |   source ./installer/install-for-launchd.sh | ||||||
|  |   installable="true" | ||||||
|  | fi | ||||||
|  | if [ -z "$installable" ]; then | ||||||
|  |   echo "" | ||||||
|  |   echo "Unknown system service init type. You must install as a system service manually." | ||||||
|  |   echo '(please file a bug with the output of "uname -a")' | ||||||
|  |   echo "" | ||||||
|  | fi | ||||||
							
								
								
									
										150
									
								
								installer/install.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								installer/install.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | 
 | ||||||
|  | set -e | ||||||
|  | set -u | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### IMPORTANT ### | ||||||
|  | ###  VERSION  ### | ||||||
|  | my_name=goldilocks | ||||||
|  | my_app_pkg_name=com.coolaj86.goldilocks.web | ||||||
|  | my_app_ver="v1.1" | ||||||
|  | my_azp_oauth3_ver="v1.2.3" | ||||||
|  | export NODE_VERSION="v8.9.3" | ||||||
|  | 
 | ||||||
|  | if [ -z "${my_tmp-}" ]; then | ||||||
|  |   my_tmp="$(mktemp -d)" | ||||||
|  |   mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name | ||||||
|  |   echo "Installing to $my_tmp (will be moved after install)" | ||||||
|  |   git clone ./ $my_tmp/opt/$my_name/lib/node_modules/$my_name | ||||||
|  |   pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name | ||||||
|  | fi | ||||||
|  | 
 | ||||||
|  | ################# | ||||||
|  | export NODE_PATH=$my_tmp/opt/$my_name/lib/node_modules | ||||||
|  | export PATH=$my_tmp/opt/$my_name/bin/:$PATH | ||||||
|  | export NPM_CONFIG_PREFIX=$my_tmp/opt/$my_name | ||||||
|  | my_npm="$NPM_CONFIG_PREFIX/bin/npm" | ||||||
|  | ################# | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | my_app_dist=$my_tmp/opt/$my_name/lib/node_modules/$my_name/dist | ||||||
|  | installer_base="https://git.coolaj86.com/coolaj86/goldilocks.js/raw/$my_app_ver" | ||||||
|  | 
 | ||||||
|  | # Backwards compat | ||||||
|  | # some scripts still use the old names | ||||||
|  | my_app_dir=$my_tmp | ||||||
|  | my_app_name=$my_name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | git checkout $my_app_ver | ||||||
|  | 
 | ||||||
|  | mkdir -p "$my_tmp/opt/$my_name"/{lib,bin,etc} | ||||||
|  | ln -s ../lib/node_modules/$my_name/bin/$my_name.js $my_tmp/opt/$my_name/bin/$my_name | ||||||
|  | ln -s ../lib/node_modules/$my_name/bin/$my_name.js $my_tmp/opt/$my_name/bin/$my_name.js | ||||||
|  | mkdir -p "$my_tmp/etc/$my_name" | ||||||
|  | chmod 775 "$my_tmp/etc/$my_name" | ||||||
|  | cat "$my_app_dist/etc/$my_name/$my_name.example.yml" > "$my_tmp/etc/$my_name/$my_name.example.yml" | ||||||
|  | chmod 664 "$my_tmp/etc/$my_name/$my_name.example.yml" | ||||||
|  | mkdir -p $my_tmp/srv/www | ||||||
|  | mkdir -p $my_tmp/var/www | ||||||
|  | mkdir -p $my_tmp/var/log/$my_name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # Helpers | ||||||
|  | # | ||||||
|  | source ./installer/sudo-cmd.sh | ||||||
|  | source ./installer/http-get.sh | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # Dependencies | ||||||
|  | # | ||||||
|  | echo $NODE_VERSION > /tmp/NODEJS_VER | ||||||
|  | http_bash "https://git.coolaj86.com/coolaj86/node-installer.sh/raw/v1.1/install.sh" | ||||||
|  | $my_npm install -g npm@4 | ||||||
|  | pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name | ||||||
|  |   $my_npm install | ||||||
|  | popd | ||||||
|  | pushd $my_tmp/opt/$my_name/lib/node_modules/$my_name/packages/assets | ||||||
|  |   OAUTH3_GIT_URL="https://git.oauth3.org/OAuth3/oauth3.js.git" | ||||||
|  |   git clone ${OAUTH3_GIT_URL} oauth3.org || true | ||||||
|  |   ln -s oauth3.org org.oauth3 | ||||||
|  |   pushd oauth3.org | ||||||
|  |     git remote set-url origin ${OAUTH3_GIT_URL} | ||||||
|  |     git checkout $my_azp_oauth3_ver | ||||||
|  |     #git pull | ||||||
|  |   popd | ||||||
|  | 
 | ||||||
|  |   mkdir -p jquery.com | ||||||
|  |   ln -s jquery.com com.jquery | ||||||
|  |   pushd jquery.com | ||||||
|  |     http_get 'https://code.jquery.com/jquery-3.1.1.js' jquery-3.1.1.js | ||||||
|  |   popd | ||||||
|  | 
 | ||||||
|  |   mkdir -p google.com | ||||||
|  |   ln -s google.com com.google | ||||||
|  |   pushd google.com | ||||||
|  |     http_get 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js' angular.1.6.2.min.js | ||||||
|  |   popd | ||||||
|  | 
 | ||||||
|  |   mkdir -p well-known | ||||||
|  |   ln -s well-known .well-known | ||||||
|  |   pushd well-known | ||||||
|  |     ln -snf ../oauth3.org/well-known/oauth3 ./oauth3 | ||||||
|  |   popd | ||||||
|  |   echo "installed dependencies" | ||||||
|  | popd | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # System Service | ||||||
|  | # | ||||||
|  | source ./installer/my-root.sh | ||||||
|  | echo "Pre-installation to $my_tmp complete, now installing to $my_root/ ..." | ||||||
|  | set +e | ||||||
|  | if type -p tree >/dev/null 2>/dev/null; then | ||||||
|  |   #tree -I "node_modules|include|share" $my_tmp | ||||||
|  |   tree -L 6 -I "include|share|npm" $my_tmp | ||||||
|  | else | ||||||
|  |   ls $my_tmp | ||||||
|  | fi | ||||||
|  | set -e | ||||||
|  | 
 | ||||||
|  | source ./installer/my-user-my-group.sh | ||||||
|  | echo "User $my_user Group $my_group" | ||||||
|  | 
 | ||||||
|  | source ./installer/install-system-service.sh | ||||||
|  | 
 | ||||||
|  | $sudo_cmd chown -R $my_user:$my_group $my_tmp/* | ||||||
|  | $sudo_cmd chown root:root $my_tmp/* | ||||||
|  | $sudo_cmd chown root:root $my_tmp | ||||||
|  | $sudo_cmd chmod 0755 $my_tmp | ||||||
|  | # don't change permissions on /, /etc, etc | ||||||
|  | $sudo_cmd rsync -a --ignore-existing $my_tmp/ $my_root/ | ||||||
|  | $sudo_cmd rsync -a --ignore-existing $my_app_dist/etc/$my_name/$my_name.yml $my_root/etc/$my_name/$my_name.yml | ||||||
|  | 
 | ||||||
|  | # Change to admin perms | ||||||
|  | $sudo_cmd chown -R $my_user:$my_group $my_root/opt/$my_name | ||||||
|  | $sudo_cmd chown -R $my_user:$my_group $my_root/var/www $my_root/srv/www | ||||||
|  | 
 | ||||||
|  | # make sure the files are all read/write for the owner and group, and then set | ||||||
|  | # the setuid and setgid bits so that any files/directories created inside these | ||||||
|  | # directories have the same owner and group. | ||||||
|  | $sudo_cmd chmod -R ug+rwX $my_root/opt/$my_name | ||||||
|  | find $my_root/opt/$my_name -type d -exec $sudo_cmd chmod ug+s {} \; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "$my_name installation complete!" | ||||||
|  | echo "" | ||||||
|  | echo "" | ||||||
|  | echo "Update the config at: /etc/$my_name/$my_name.yml" | ||||||
|  | echo "" | ||||||
|  | echo "Unistall: rm -rf /srv/$my_name/ /var/$my_name/ /etc/$my_name/ /opt/$my_name/ /var/log/$my_name/ /etc/tmpfiles.d/$my_name.conf /etc/systemd/system/$my_name.service /etc/ssl/$my_name" | ||||||
							
								
								
									
										8
									
								
								installer/my-root.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								installer/my-root.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | # something or other about android and tmux using PREFIX | ||||||
|  | #: "${PREFIX:=''}" | ||||||
|  | my_root="" | ||||||
|  | if [ -z "${PREFIX-}" ]; then | ||||||
|  |   my_root="" | ||||||
|  | else | ||||||
|  |   my_root="$PREFIX" | ||||||
|  | fi | ||||||
							
								
								
									
										19
									
								
								installer/my-user-my-group.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								installer/my-user-my-group.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | if type -p adduser >/dev/null 2>/dev/null; then | ||||||
|  |   if [ -z "$(cat $my_root/etc/passwd | grep $my_app_name)" ]; then | ||||||
|  |     $sudo_cmd adduser --home $my_root/opt/$my_app_name --gecos '' --disabled-password $my_app_name | ||||||
|  |   fi | ||||||
|  |   my_user=$my_app_name | ||||||
|  |   my_group=$my_app_name | ||||||
|  | elif [ -n "$(cat /etc/passwd | grep www-data:)" ]; then | ||||||
|  |   # Linux (Ubuntu) | ||||||
|  |   my_user=www-data | ||||||
|  |   my_group=www-data | ||||||
|  | elif [ -n "$(cat /etc/passwd | grep _www:)" ]; then | ||||||
|  |   # Mac | ||||||
|  |   my_user=_www | ||||||
|  |   my_group=_www | ||||||
|  | else | ||||||
|  |   # Unsure | ||||||
|  |   my_user=$(whoami) | ||||||
|  |   my_group=$(id -g -n) | ||||||
|  | fi | ||||||
							
								
								
									
										7
									
								
								installer/sudo-cmd.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								installer/sudo-cmd.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | # Not every platform has or needs sudo, gotta save them O(1)s... | ||||||
|  | sudo_cmd="" | ||||||
|  | set +e | ||||||
|  | if type -p sudo >/dev/null 2>/dev/null; then | ||||||
|  |   ((EUID)) && [[ -z "${ANDROID_ROOT-}" ]] && sudo_cmd="sudo" | ||||||
|  | fi | ||||||
|  | set -e | ||||||
							
								
								
									
										585
									
								
								lib/admin/apis.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										585
									
								
								lib/admin/apis.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,585 @@ | |||||||
|  | '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; | ||||||
|  | }; | ||||||
							
								
								
									
										398
									
								
								lib/admin/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								lib/admin/config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,398 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var validator = new (require('jsonschema').Validator)(); | ||||||
|  | var recase = require('recase').create({}); | ||||||
|  | 
 | ||||||
|  | var portSchema = { type: 'number', minimum: 1, maximum: 65535 }; | ||||||
|  | 
 | ||||||
|  | var moduleSchemas = { | ||||||
|  |   // the proxy module is common to basically all categories.
 | ||||||
|  |   proxy: { | ||||||
|  |     type: 'object' | ||||||
|  |   , oneOf: [ | ||||||
|  |       { required: [ 'address' ] } | ||||||
|  |     , { required: [ 'port' ] } | ||||||
|  |     ] | ||||||
|  |   , properties: { | ||||||
|  |       address: { type: 'string' } | ||||||
|  |     , host:    { type: 'string' } | ||||||
|  |     , port:    portSchema | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // redirect and static modules are for HTTP
 | ||||||
|  | , redirect: { | ||||||
|  |     type: 'object' | ||||||
|  |   , required: [ 'to', 'from' ] | ||||||
|  |   , properties: { | ||||||
|  |       to:     { type: 'string'} | ||||||
|  |     , from:   { type: 'string'} | ||||||
|  |     , status: { type: 'integer', minimum: 1, maximum: 999 } | ||||||
|  |   , } | ||||||
|  |   } | ||||||
|  | , static: { | ||||||
|  |     type: 'object' | ||||||
|  |   , required: [ 'root' ] | ||||||
|  |   , properties: { | ||||||
|  |       root: { type: 'string' } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // the acme module is for TLS
 | ||||||
|  | , acme: { | ||||||
|  |     type: 'object' | ||||||
|  |   , required: [ 'email' ] | ||||||
|  |   , properties: { | ||||||
|  |       email:          { type: 'string' } | ||||||
|  |     , server:         { type: 'string' } | ||||||
|  |     , challenge_type: { type: 'string' } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // the dns control modules for DDNS
 | ||||||
|  | , 'dns@oauth3.org': { | ||||||
|  |     type: 'object' | ||||||
|  |   , required: [ 'token_id' ] | ||||||
|  |   , properties: { | ||||||
|  |       token_id: { type: 'string' } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | // forward is basically the same as proxy, but specifies the relevant incoming port(s).
 | ||||||
|  | // only allows for the raw transport layers (TCP/UDP)
 | ||||||
|  | moduleSchemas.forward = JSON.parse(JSON.stringify(moduleSchemas.proxy)); | ||||||
|  | moduleSchemas.forward.required = [ 'ports' ]; | ||||||
|  | moduleSchemas.forward.properties.ports = { type: 'array', items: portSchema }; | ||||||
|  | 
 | ||||||
|  | Object.keys(moduleSchemas).forEach(function (name) { | ||||||
|  |   var schema = moduleSchemas[name]; | ||||||
|  |   schema.id = '/modules/'+name; | ||||||
|  |   schema.required = ['id', 'type'].concat(schema.required || []); | ||||||
|  |   schema.properties.id   = { type: 'string' }; | ||||||
|  |   schema.properties.type = { type: 'string', const: name }; | ||||||
|  |   validator.addSchema(schema, schema.id); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function addDomainRequirement(itemSchema) { | ||||||
|  |   var result = Object.assign({}, itemSchema); | ||||||
|  |   result.required = (result.required || []).concat('domains'); | ||||||
|  |   result.properties = Object.assign({}, result.properties); | ||||||
|  |   result.properties.domains = { type: 'array', items: { type: 'string' }, minLength: 1}; | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function toSchemaRef(name) { | ||||||
|  |   return { '$ref': '/modules/'+name }; | ||||||
|  | } | ||||||
|  | var moduleRefs = { | ||||||
|  |   http: [ 'proxy', 'static', 'redirect' ].map(toSchemaRef) | ||||||
|  | , tls:  [ 'proxy', 'acme' ].map(toSchemaRef) | ||||||
|  | , tcp:  [ 'forward' ].map(toSchemaRef) | ||||||
|  | , udp:  [ 'forward' ].map(toSchemaRef) | ||||||
|  | , ddns: [ 'dns@oauth3.org' ].map(toSchemaRef) | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // TCP is a bit special in that it has a module that doesn't operate based on domain name
 | ||||||
|  | // (ie forward), and a modules that does (ie proxy). It therefore has different module
 | ||||||
|  | // when part of the `domains` config, and when not part of the `domains` config the proxy
 | ||||||
|  | // modules must have the `domains` property while forward should not have it.
 | ||||||
|  | moduleRefs.tcp.push(addDomainRequirement(toSchemaRef('proxy'))); | ||||||
|  | 
 | ||||||
|  | var domainSchema = { | ||||||
|  |   type: 'array' | ||||||
|  | , items: { | ||||||
|  |     type: 'object' | ||||||
|  |   , properties: { | ||||||
|  |       id:      { type: 'string' } | ||||||
|  |     , names:   { type: 'array', items: { type: 'string' }, minLength: 1} | ||||||
|  |     , modules: { | ||||||
|  |         type: 'object' | ||||||
|  |       , properties: { | ||||||
|  |           tls:  { type: 'array', items: { oneOf: moduleRefs.tls }} | ||||||
|  |         , http: { type: 'array', items: { oneOf: moduleRefs.http }} | ||||||
|  |         , ddns: { type: 'array', items: { oneOf: moduleRefs.ddns }} | ||||||
|  |         , tcp:  { type: 'array', items: { oneOf: ['proxy'].map(toSchemaRef)}} | ||||||
|  |         } | ||||||
|  |       , additionalProperties: false | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var httpSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , properties: { | ||||||
|  |     modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.http }) } | ||||||
|  | 
 | ||||||
|  |     // These properties should be snake_case to match the API and config format
 | ||||||
|  |   , primary_domain: { type: 'string' } | ||||||
|  |   , allow_insecure: { type: 'boolean' } | ||||||
|  |   , trust_proxy:    { type: 'boolean' } | ||||||
|  | 
 | ||||||
|  |     // these are forbidden deprecated settings.
 | ||||||
|  |   , bind:    { not: {} } | ||||||
|  |   , domains: { not: {} } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var tlsSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , properties: { | ||||||
|  |     modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.tls }) } | ||||||
|  | 
 | ||||||
|  |     // these are forbidden deprecated settings.
 | ||||||
|  |   , acme:    { not: {} } | ||||||
|  |   , bind:    { not: {} } | ||||||
|  |   , domains: { not: {} } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var tcpSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , required: [ 'bind' ] | ||||||
|  | , properties: { | ||||||
|  |     bind:    { type: 'array', items: portSchema, minLength: 1 } | ||||||
|  |   , modules: { type: 'array', items: { oneOf: moduleRefs.tcp }} | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var udpSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , properties: { | ||||||
|  |     bind:    { type: 'array', items: portSchema } | ||||||
|  |   , modules: { type: 'array', items: { oneOf: moduleRefs.udp }} | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var mdnsSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , required: [ 'port', 'broadcast', 'ttl' ] | ||||||
|  | , properties: { | ||||||
|  |     port:      portSchema | ||||||
|  |   , broadcast: { type: 'string' } | ||||||
|  |   , ttl:       { type: 'integer', minimum: 0, maximum: 2147483647 } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var tunnelSvrSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , properties: { | ||||||
|  |     servernames: { type: 'array', items: { type: 'string' }} | ||||||
|  |   , secret:      { type: 'string' } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var ddnsSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , properties: { | ||||||
|  |     loopback: { | ||||||
|  |       type: 'object' | ||||||
|  |     , required: [ 'type', 'domain' ] | ||||||
|  |     , properties: { | ||||||
|  |         type:   { type: 'string', const: 'tunnel@oauth3.org' } | ||||||
|  |       , domain: { type: 'string'} | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   , tunnel: { | ||||||
|  |       type: 'object' | ||||||
|  |     , required: [ 'type', 'token_id' ] | ||||||
|  |     , properties: { | ||||||
|  |         type:  { type: 'string', const: 'tunnel@oauth3.org' } | ||||||
|  |       , token_id: { type: 'string'} | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   , modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.ddns })} | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | var socks5Schema = { | ||||||
|  |   type: 'object' | ||||||
|  | , properties: { | ||||||
|  |     enabled: { type: 'boolean' } | ||||||
|  |   , port:    portSchema | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | var deviceSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , properties: { | ||||||
|  |     hostname: { type: 'string' } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var mainSchema = { | ||||||
|  |   type: 'object' | ||||||
|  | , required: [ 'domains', 'http', 'tls', 'tcp', 'udp', 'mdns', 'ddns' ] | ||||||
|  | , properties: { | ||||||
|  |     domains:domainSchema | ||||||
|  |   , http:   httpSchema | ||||||
|  |   , tls:    tlsSchema | ||||||
|  |   , tcp:    tcpSchema | ||||||
|  |   , udp:    udpSchema | ||||||
|  |   , mdns:   mdnsSchema | ||||||
|  |   , ddns:   ddnsSchema | ||||||
|  |   , socks5: socks5Schema | ||||||
|  |   , device: deviceSchema | ||||||
|  |   , tunnel_server: tunnelSvrSchema | ||||||
|  |   } | ||||||
|  | , additionalProperties: false | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function validate(config) { | ||||||
|  |   return validator.validate(recase.snakeCopy(config), mainSchema).errors; | ||||||
|  | } | ||||||
|  | module.exports.validate = validate; | ||||||
|  | 
 | ||||||
|  | class IdList extends Array { | ||||||
|  |   constructor(rawList) { | ||||||
|  |     super(); | ||||||
|  |     if (Array.isArray(rawList)) { | ||||||
|  |       Object.assign(this, JSON.parse(JSON.stringify(rawList))); | ||||||
|  |     } | ||||||
|  |     this._itemName = 'item'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   findId(id) { | ||||||
|  |     return Array.prototype.find.call(this, function (dom) { | ||||||
|  |       return dom.id === id; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   add(item) { | ||||||
|  |     item.id = require('crypto').randomBytes(4).toString('hex'); | ||||||
|  |     this.push(item); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   update(id, update) { | ||||||
|  |     var item = this.findId(id); | ||||||
|  |     if (!item) { | ||||||
|  |       var error = new Error("no "+this._itemName+" with ID '"+id+"'"); | ||||||
|  |       error.statusCode = 404; | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |     Object.assign(this.findId(id), update); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   remove(id) { | ||||||
|  |     var index = this.findIndex(function (dom) { | ||||||
|  |       return dom.id === id; | ||||||
|  |     }); | ||||||
|  |     if (index < 0) { | ||||||
|  |       var error = new Error("no "+this._itemName+" with ID '"+id+"'"); | ||||||
|  |       error.statusCode = 404; | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |     this.splice(index, 1); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | class ModuleList extends IdList { | ||||||
|  |   constructor(rawList) { | ||||||
|  |     super(rawList); | ||||||
|  |     this._itemName = 'module'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   add(mod) { | ||||||
|  |     if (!mod.type) { | ||||||
|  |       throw new Error("module must have a 'type' defined"); | ||||||
|  |     } | ||||||
|  |     if (!moduleSchemas[mod.type]) { | ||||||
|  |       throw new Error("invalid module type '"+mod.type+"'"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     mod.id = require('crypto').randomBytes(4).toString('hex'); | ||||||
|  |     this.push(mod); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | class DomainList extends IdList { | ||||||
|  |   constructor(rawList) { | ||||||
|  |     super(rawList); | ||||||
|  |     this._itemName = 'domain'; | ||||||
|  |     this.forEach(function (dom) { | ||||||
|  |       dom.modules = { | ||||||
|  |         http: new ModuleList((dom.modules || {}).http) | ||||||
|  |       , tls:  new ModuleList((dom.modules || {}).tls) | ||||||
|  |       , ddns: new ModuleList((dom.modules || {}).ddns) | ||||||
|  |       , tcp:  new ModuleList((dom.modules || {}).tcp) | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   add(dom) { | ||||||
|  |     if (!Array.isArray(dom.names) || !dom.names.length) { | ||||||
|  |       throw new Error("domains must have a non-empty array for 'names'"); | ||||||
|  |     } | ||||||
|  |     if (dom.names.some(function (name) { return typeof name !== 'string'; })) { | ||||||
|  |       throw new Error("all domain names must be strings"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var modLists = { | ||||||
|  |       http: new ModuleList() | ||||||
|  |     , tls:  new ModuleList() | ||||||
|  |     , ddns: new ModuleList() | ||||||
|  |     , tcp:  new ModuleList() | ||||||
|  |     }; | ||||||
|  |     // We add these after instead of in the constructor to run the validation and manipulation
 | ||||||
|  |     // in the ModList add function since these are all new modules.
 | ||||||
|  |     if (dom.modules) { | ||||||
|  |       Object.keys(modLists).forEach(function (key) { | ||||||
|  |         if (Array.isArray(dom.modules[key])) { | ||||||
|  |           dom.modules[key].forEach(modLists[key].add, modLists[key]); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     dom.id = require('crypto').randomBytes(4).toString('hex'); | ||||||
|  |     dom.modules = modLists; | ||||||
|  |     this.push(dom); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class ConfigChanger { | ||||||
|  |   constructor(start) { | ||||||
|  |     Object.assign(this, JSON.parse(JSON.stringify(start))); | ||||||
|  |     delete this.device; | ||||||
|  |     delete this.debug; | ||||||
|  | 
 | ||||||
|  |     this.domains = new DomainList(this.domains); | ||||||
|  |     this.http.modules = new ModuleList(this.http.modules); | ||||||
|  |     this.tls.modules  = new ModuleList(this.tls.modules); | ||||||
|  |     this.tcp.modules  = new ModuleList(this.tcp.modules); | ||||||
|  |     this.udp.modules  = new ModuleList(this.udp.modules); | ||||||
|  |     this.ddns.modules = new ModuleList(this.ddns.modules); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   update(update) { | ||||||
|  |     var self = this; | ||||||
|  | 
 | ||||||
|  |     if (update.domains) { | ||||||
|  |       update.domains.forEach(self.domains.add, self.domains); | ||||||
|  |     } | ||||||
|  |     [ 'http', 'tls', 'tcp', 'udp', 'ddns' ].forEach(function (name) { | ||||||
|  |       if (update[name] && update[name].modules) { | ||||||
|  |         update[name].modules.forEach(self[name].modules.add, self[name].modules); | ||||||
|  |         delete update[name].modules; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     function mergeSettings(orig, changes) { | ||||||
|  |       Object.keys(changes).forEach(function (key) { | ||||||
|  |         // TODO: use an API that can properly handle updating arrays.
 | ||||||
|  |         if (!changes[key] || (typeof changes[key] !== 'object') || Array.isArray(changes[key])) { | ||||||
|  |           orig[key] = changes[key]; | ||||||
|  |         } | ||||||
|  |         else if (!orig[key] || typeof orig[key] !== 'object') { | ||||||
|  |           orig[key] = changes[key]; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |           mergeSettings(orig[key], changes[key]); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     mergeSettings(this, update); | ||||||
|  | 
 | ||||||
|  |     return validate(this); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   validate() { | ||||||
|  |     return validate(this); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports.ConfigChanger = ConfigChanger; | ||||||
							
								
								
									
										31
									
								
								lib/admin/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								lib/admin/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | var adminDomains = [ | ||||||
|  |   'localhost.alpha.daplie.me' | ||||||
|  | , 'localhost.admin.daplie.me' | ||||||
|  | , 'alpha.localhost.daplie.me' | ||||||
|  | , 'admin.localhost.daplie.me' | ||||||
|  | , 'localhost.daplie.invalid' | ||||||
|  | ]; | ||||||
|  | module.exports.adminDomains = adminDomains; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (deps, conf) { | ||||||
|  |   'use strict'; | ||||||
|  | 
 | ||||||
|  |   var path = require('path'); | ||||||
|  |   var express = require('express'); | ||||||
|  |   var app = express(); | ||||||
|  | 
 | ||||||
|  |   var apis = require('./apis').create(deps, conf); | ||||||
|  |   app.use('/api/goldilocks@daplie.com', apis); | ||||||
|  |   app.use('/api/com.daplie.goldilocks', apis); | ||||||
|  | 
 | ||||||
|  |   // Serve the static assets for the UI (even though it probably won't be used very
 | ||||||
|  |   // often since it only works on localhost domains). Note that we are using the default
 | ||||||
|  |   // .well-known directory from the oauth3 library even though it indicates we have
 | ||||||
|  |   // capabilities we don't support because it's simpler and it's unlikely anything will
 | ||||||
|  |   // actually use it to determine our API (it is needed to log into the web page).
 | ||||||
|  |   app.use('/.well-known', express.static(path.join(__dirname, '../../packages/assets/well-known'))); | ||||||
|  |   app.use('/assets',      express.static(path.join(__dirname, '../../packages/assets'))); | ||||||
|  |   app.use('/',            express.static(path.join(__dirname, '../../admin/public'))); | ||||||
|  | 
 | ||||||
|  |   return require('http').createServer(app); | ||||||
|  | }; | ||||||
							
								
								
									
										492
									
								
								lib/app.js
									
									
									
									
									
								
							
							
						
						
									
										492
									
								
								lib/app.js
									
									
									
									
									
								
							| @ -1,492 +0,0 @@ | |||||||
| '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; |  | ||||||
|   var tun; |  | ||||||
|   var request; |  | ||||||
| 
 |  | ||||||
|   /* |  | ||||||
|   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'); |  | ||||||
|     request = request || PromiseA.promisify(require('request')); |  | ||||||
| 
 |  | ||||||
|     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: request |  | ||||||
|     , options: opts |  | ||||||
|     , api: { |  | ||||||
|         // TODO move loopback to oauth3.api('tunnel:loopback')
 |  | ||||||
|         loopback: function (deps, session, opts2) { |  | ||||||
|           var crypto = require('crypto'); |  | ||||||
|           var token = crypto.randomBytes(16).toString('hex'); |  | ||||||
|           var keyAuthorization = crypto.randomBytes(16).toString('hex'); |  | ||||||
|           var nonce = crypto.randomBytes(16).toString('hex'); |  | ||||||
| 
 |  | ||||||
|           // TODO set token and keyAuthorization to /.well-known/cloud-challenge/:token
 |  | ||||||
|           return request({ |  | ||||||
|             method: 'POST' |  | ||||||
|           , url: 'https://oauth3.org/api/org.oauth3.tunnel/loopback' |  | ||||||
|           , json: { |  | ||||||
|               address: opts2.address |  | ||||||
|             , port: opts2.port |  | ||||||
|             , token: token |  | ||||||
|             , keyAuthorization: keyAuthorization |  | ||||||
|             , servername: opts2.servername |  | ||||||
|             , nonce: nonce |  | ||||||
|             , scheme: 'https' |  | ||||||
|             , iat: Date.now() |  | ||||||
|             } |  | ||||||
|           }).then(function (result) { |  | ||||||
|             // TODO this will always fail at the moment
 |  | ||||||
|             console.log('loopback result:'); |  | ||||||
|             return result; |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       , tunnel: function (deps, session) { |  | ||||||
|           // TODO save session to config and turn tunnel on
 |  | ||||||
|           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(result.jwt.split('.')[1], 'base64').toString('ascii')).aud + '/'); |  | ||||||
|                 } |  | ||||||
|                 var opts3 = { |  | ||||||
|                   token: result.jwt |  | ||||||
|                 , stunneld: result.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 |  | ||||||
|                 }; |  | ||||||
| 
 |  | ||||||
|                 if (tun) { |  | ||||||
|                   if (tun.append) { |  | ||||||
|                     tun.append(result.jwt); |  | ||||||
|                   } |  | ||||||
|                   else if (tun.end) { |  | ||||||
|                     tun.end(); |  | ||||||
|                     tun = null; |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (!tun) { |  | ||||||
|                   tun = stunnel.connect(opts3); |  | ||||||
|                   opts.tun = true; |  | ||||||
|                 } |  | ||||||
|               }); |  | ||||||
|             /* |  | ||||||
|             }); |  | ||||||
|             */ |  | ||||||
|           }); |  | ||||||
|           //, { 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); |  | ||||||
|     */ |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
							
								
								
									
										54
									
								
								lib/check-ports.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								lib/check-ports.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | '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
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								lib/ddns.js
									
									
									
									
									
								
							| @ -1,88 +0,0 @@ | |||||||
| '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); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
							
								
								
									
										122
									
								
								lib/ddns/challenge-responder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								lib/ddns/challenge-responder.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,122 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | // Much of this file was based on the `le-challenge-ddns` library (which we are not using
 | ||||||
|  | // here because it's method of setting records requires things we don't really want).
 | ||||||
|  | module.exports.create = function (deps, conf, utils) { | ||||||
|  | 
 | ||||||
|  |   function getReleventSessionId(domain) { | ||||||
|  |     var sessId; | ||||||
|  | 
 | ||||||
|  |     utils.iterateAllModules(function (mod, domainList) { | ||||||
|  |       // We return a truthy value in these cases because of the way the iterate function
 | ||||||
|  |       // handles modules grouped by domain. By returning true we are saying these domains
 | ||||||
|  |       // are "handled" and so if there are multiple modules we won't be given the rest.
 | ||||||
|  |       if (sessId) { return true; } | ||||||
|  |       if (domainList.indexOf(domain) < 0) { return true; } | ||||||
|  | 
 | ||||||
|  |       // But if the domains are relevant but we don't know how to handle the module we
 | ||||||
|  |       // return false to allow us to look at any other modules that might exist here.
 | ||||||
|  |       if (mod.type !== 'dns@oauth3.org')  { return false; } | ||||||
|  | 
 | ||||||
|  |       sessId = mod.tokenId || mod.token_id; | ||||||
|  |       return true; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return sessId; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function get(args, domain, challenge, done) { | ||||||
|  |     done(new Error("Challenge.get() does not need an implementation for dns-01. (did you mean Challenge.loopback?)")); | ||||||
|  |   } | ||||||
|  |   // same as get, but external
 | ||||||
|  |   function loopback(args, domain, challenge, done) { | ||||||
|  |     var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; | ||||||
|  |     require('dns').resolveTxt(challengeDomain, done); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var activeChallenges = {}; | ||||||
|  |   async function removeAsync(args, domain) { | ||||||
|  |     var data = activeChallenges[domain]; | ||||||
|  |     if (!data) { | ||||||
|  |       console.warn(new Error('cannot remove DNS challenge for ' + domain + ': already removed')); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var session = await utils.getSession(data.sessId); | ||||||
|  |     var directives = await deps.OAUTH3.discover(session.token.aud); | ||||||
|  |     var apiOpts = { | ||||||
|  |       api: 'dns.unset' | ||||||
|  |     , session: session | ||||||
|  |     , type: 'TXT' | ||||||
|  |     , value: data.keyAuthDigest | ||||||
|  |     }; | ||||||
|  |     await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, data.splitDomain)); | ||||||
|  | 
 | ||||||
|  |     delete activeChallenges[domain]; | ||||||
|  |   } | ||||||
|  |   async function setAsync(args, domain, challenge, keyAuth) { | ||||||
|  |     if (activeChallenges[domain]) { | ||||||
|  |       await removeAsync(args, domain, challenge); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var sessId = getReleventSessionId(domain); | ||||||
|  |     if (!sessId) { | ||||||
|  |       throw new Error('no DDNS module handles the domain ' + domain); | ||||||
|  |     } | ||||||
|  |     var session = await utils.getSession(sessId); | ||||||
|  |     var directives = await deps.OAUTH3.discover(session.token.aud); | ||||||
|  | 
 | ||||||
|  |     // I'm not sure what role challenge is supposed to play since even in the library
 | ||||||
|  |     // this code is based on it was never used, but check for it anyway because ...
 | ||||||
|  |     if (!challenge || keyAuth) { | ||||||
|  |       console.warn(new Error('DDNS challenge missing challenge or keyAuth')); | ||||||
|  |     } | ||||||
|  |     var keyAuthDigest = require('crypto').createHash('sha256').update(keyAuth || '').digest('base64') | ||||||
|  |       .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); | ||||||
|  | 
 | ||||||
|  |     var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; | ||||||
|  |     var splitDomain = (await utils.splitDomains(directives.api, [challengeDomain]))[0]; | ||||||
|  | 
 | ||||||
|  |     var apiOpts = { | ||||||
|  |       api: 'dns.set' | ||||||
|  |     , session: session | ||||||
|  |     , type: 'TXT' | ||||||
|  |     , value: keyAuthDigest | ||||||
|  |     , ttl: args.ttl || 0 | ||||||
|  |     }; | ||||||
|  |     await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, splitDomain)); | ||||||
|  | 
 | ||||||
|  |     activeChallenges[domain] = { | ||||||
|  |       sessId | ||||||
|  |     , keyAuthDigest | ||||||
|  |     , splitDomain | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return new Promise(res => setTimeout(res, 1000)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // It might be slightly easier to use arguments and apply, but the library that will use
 | ||||||
|  |   // this function counts the arguments we expect.
 | ||||||
|  |   function set(a, b, c, d, done) { | ||||||
|  |     setAsync(a, b, c, d).then(result => done(null, result), done); | ||||||
|  |   } | ||||||
|  |   function remove(a, b, c, done) { | ||||||
|  |     removeAsync(a, b, c).then(result => done(null, result), done); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function getOptions() { | ||||||
|  |     return { | ||||||
|  |       oauth3: 'oauth3.org' | ||||||
|  |     , debug: conf.debug | ||||||
|  |     , acmeChallengeDns: '_acme-challenge.' | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     getOptions | ||||||
|  |   , set | ||||||
|  |   , get | ||||||
|  |   , remove | ||||||
|  |   , loopback | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										132
									
								
								lib/ddns/dns-ctrl.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								lib/ddns/dns-ctrl.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (deps, conf, utils) { | ||||||
|  |   function dnsType(addr) { | ||||||
|  |     if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) { | ||||||
|  |       return 'A'; | ||||||
|  |     } | ||||||
|  |     if (-1 !== addr.indexOf(':') && /^[a-f:\.\d]+$/i.test(addr)) { | ||||||
|  |       return 'AAAA'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function setDeviceAddress(session, addr, domains) { | ||||||
|  |     var directives = await deps.OAUTH3.discover(session.token.aud); | ||||||
|  | 
 | ||||||
|  |     // Set the address of the device to our public address.
 | ||||||
|  |     await deps.request({ | ||||||
|  |       url: deps.OAUTH3.url.normalize(directives.api)+'/api/com.daplie.domains/acl/devices/' + conf.device.hostname | ||||||
|  |     , method: 'POST' | ||||||
|  |     , headers: { | ||||||
|  |         'Authorization': 'Bearer ' + session.refresh_token | ||||||
|  |       , 'Accept': 'application/json; charset=utf-8' | ||||||
|  |       } | ||||||
|  |     , json: { | ||||||
|  |         addresses: [ | ||||||
|  |           { value: addr, type:  dnsType(addr) } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Then update all of the records attached to our hostname, first removing the old records
 | ||||||
|  |     // to remove the reference to the old address, then creating new records for the same domains
 | ||||||
|  |     // using our new address.
 | ||||||
|  |     var allDns = await deps.OAUTH3.api(directives.api, {session: session, api: 'dns.list'}); | ||||||
|  |     var ourDns = allDns.filter(function (record) { | ||||||
|  |       if (record.device !== conf.device.hostname) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if ([ 'A', 'AAAA' ].indexOf(record.type) < 0) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       return domains.indexOf(record.host) !== -1; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Of all the DNS records referring to our device and the current list of domains determine
 | ||||||
|  |     // which domains have records with outdated address, and which ones we can just leave be
 | ||||||
|  |     // without updating them.
 | ||||||
|  |     var badAddrDomains = ourDns.filter(function (record) { | ||||||
|  |       return record.value !== addr; | ||||||
|  |     }).map(record => record.host); | ||||||
|  |     var goodAddrDomains = ourDns.filter(function (record) { | ||||||
|  |       return record.value === addr && badAddrDomains.indexOf(record.host) < 0; | ||||||
|  |     }).map(record => record.host); | ||||||
|  |     var requiredUpdates = domains.filter(function (domain) { | ||||||
|  |       return goodAddrDomains.indexOf(domain) < 0; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     var oldDns = await utils.splitDomains(directives.api, badAddrDomains); | ||||||
|  |     var common = { | ||||||
|  |       api: 'devices.detach' | ||||||
|  |     , session: session | ||||||
|  |     , device: conf.device.hostname | ||||||
|  |     }; | ||||||
|  |     await deps.PromiseA.all(oldDns.map(function (record) { | ||||||
|  |       return deps.OAUTH3.api(directives.api, Object.assign({}, common, record)); | ||||||
|  |     })); | ||||||
|  |     if (conf.debug && badAddrDomains.length) { | ||||||
|  |       console.log('removed bad DNS records for ' + badAddrDomains.join(', ')); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var newDns = await utils.splitDomains(directives.api, requiredUpdates); | ||||||
|  |     common = { | ||||||
|  |       api: 'devices.attach' | ||||||
|  |     , session: session | ||||||
|  |     , device: conf.device.hostname | ||||||
|  |     , ip: addr | ||||||
|  |     , ttl: 300 | ||||||
|  |     }; | ||||||
|  |     await deps.PromiseA.all(newDns.map(function (record) { | ||||||
|  |       return deps.OAUTH3.api(directives.api, Object.assign({}, common, record)); | ||||||
|  |     })); | ||||||
|  |     if (conf.debug && requiredUpdates.length) { | ||||||
|  |       console.log('set new DNS records for ' + requiredUpdates.join(', ')); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function getDeviceAddresses(session) { | ||||||
|  |     var directives = await deps.OAUTH3.discover(session.token.aud); | ||||||
|  | 
 | ||||||
|  |     var result = await deps.request({ | ||||||
|  |       url: deps.OAUTH3.url.normalize(directives.api)+'/api/org.oauth3.dns/acl/devices' | ||||||
|  |     , method: 'GET' | ||||||
|  |     , headers: { | ||||||
|  |         'Authorization': 'Bearer ' + session.refresh_token | ||||||
|  |       , 'Accept': 'application/json; charset=utf-8' | ||||||
|  |       } | ||||||
|  |     , json: true | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (!result.body) { | ||||||
|  |       throw new Error('No response body in request for device addresses'); | ||||||
|  |     } | ||||||
|  |     if (result.body.error) { | ||||||
|  |       throw Object.assign(new Error('error getting device list'), result.body.error); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var dev = result.body.devices.filter(function (dev) { | ||||||
|  |       return dev.name === conf.device.hostname; | ||||||
|  |     })[0]; | ||||||
|  |     return (dev || {}).addresses || []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function removeDomains(session, domains) { | ||||||
|  |     var directives = await deps.OAUTH3.discover(session.token.aud); | ||||||
|  | 
 | ||||||
|  |     var oldDns = await utils.splitDomains(directives.api, domains); | ||||||
|  |     var common = { | ||||||
|  |       api: 'devices.detach' | ||||||
|  |     , session: session | ||||||
|  |     , device: conf.device.hostname | ||||||
|  |     }; | ||||||
|  |     await deps.PromiseA.all(oldDns.map(function (record) { | ||||||
|  |       return deps.OAUTH3.api(directives.api, Object.assign({}, common, record)); | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     getDeviceAddresses | ||||||
|  |   , setDeviceAddress | ||||||
|  |   , removeDomains | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										326
									
								
								lib/ddns/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								lib/ddns/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,326 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (deps, conf) { | ||||||
|  |   var dns = deps.PromiseA.promisifyAll(require('dns')); | ||||||
|  |   var network = deps.PromiseA.promisifyAll(deps.recase.camelCopy(require('network'))); | ||||||
|  |   var equal = require('deep-equal'); | ||||||
|  | 
 | ||||||
|  |   var utils = require('./utils').create(deps, conf); | ||||||
|  |   var loopback = require('./loopback').create(deps, conf, utils); | ||||||
|  |   var dnsCtrl = require('./dns-ctrl').create(deps, conf, utils); | ||||||
|  |   var challenge = require('./challenge-responder').create(deps, conf, utils); | ||||||
|  |   var tunnelClients = require('./tunnel-client-manager').create(deps, conf, utils); | ||||||
|  | 
 | ||||||
|  |   var loopbackDomain; | ||||||
|  | 
 | ||||||
|  |   var tunnelActive = false; | ||||||
|  |   async function startTunnel(tunnelSession, mod, domainList) { | ||||||
|  |     try { | ||||||
|  |       var dnsSession = await utils.getSession(mod.tokenId); | ||||||
|  |       var tunnelDomain = await tunnelClients.start(tunnelSession || dnsSession, domainList); | ||||||
|  | 
 | ||||||
|  |       var addrList; | ||||||
|  |       try { | ||||||
|  |         addrList = await dns.resolve4Async(tunnelDomain); | ||||||
|  |       } catch (e) {} | ||||||
|  |       if (!addrList || !addrList.length) { | ||||||
|  |         try { | ||||||
|  |           addrList = await dns.resolve6Async(tunnelDomain); | ||||||
|  |         } catch (e) {} | ||||||
|  |       } | ||||||
|  |       if (!addrList || !addrList.length || !addrList[0]) { | ||||||
|  |         throw new Error('failed to lookup IP for tunnel domain "' + tunnelDomain + '"'); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (!mod.disabled) { | ||||||
|  |         await dnsCtrl.setDeviceAddress(dnsSession, addrList[0], domainList); | ||||||
|  |       } | ||||||
|  |     } catch (err) { | ||||||
|  |       console.log('error starting tunnel for', domainList.join(', ')); | ||||||
|  |       console.log(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   async function connectAllTunnels() { | ||||||
|  |     var tunnelSession; | ||||||
|  |     if (conf.ddns.tunnel) { | ||||||
|  |       // In the case of a non-existant token, I'm not sure if we want to throw here and prevent
 | ||||||
|  |       // any tunnel connections, or if we  want to ignore it and fall back to the DNS tokens
 | ||||||
|  |       tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await utils.iterateAllModules(function (mod, domainList) { | ||||||
|  |       if (mod.type !== 'dns@oauth3.org') { return null; } | ||||||
|  | 
 | ||||||
|  |       return startTunnel(tunnelSession, mod, domainList); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     tunnelActive = true; | ||||||
|  |   } | ||||||
|  |   async function disconnectTunnels() { | ||||||
|  |     tunnelClients.disconnect(); | ||||||
|  |     tunnelActive = false; | ||||||
|  |     await Promise.resolve(); | ||||||
|  |   } | ||||||
|  |   async function checkTunnelTokens() { | ||||||
|  |     var oldTokens = tunnelClients.current(); | ||||||
|  | 
 | ||||||
|  |     var newTokens = await utils.iterateAllModules(function checkTokens(mod, domainList) { | ||||||
|  |       if (mod.type !== 'dns@oauth3.org') { return null; } | ||||||
|  | 
 | ||||||
|  |       var domainStr = domainList.slice().sort().join(','); | ||||||
|  |       // If there is already a token handling exactly the domains this modules
 | ||||||
|  |       // needs handled remove it from the list of tokens to be removed. Otherwise
 | ||||||
|  |       // return the module and domain list so we can get new tokens.
 | ||||||
|  |       if (oldTokens[domainStr]) { | ||||||
|  |         delete oldTokens[domainStr]; | ||||||
|  |       } else { | ||||||
|  |         return Promise.resolve({ mod, domainList }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await Promise.all(Object.values(oldTokens).map(tunnelClients.remove)); | ||||||
|  | 
 | ||||||
|  |     if (!newTokens.length) { return; } | ||||||
|  | 
 | ||||||
|  |     var tunnelSession; | ||||||
|  |     if (conf.ddns.tunnel) { | ||||||
|  |       // In the case of a non-existant token, I'm not sure if we want to throw here and prevent
 | ||||||
|  |       // any tunnel connections, or if we  want to ignore it and fall back to the DNS tokens
 | ||||||
|  |       tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await Promise.all(newTokens.map(function ({mod, domainList}) { | ||||||
|  |       return startTunnel(tunnelSession, mod, domainList); | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var localAddr, gateway; | ||||||
|  |   async function checkNetworkEnv() { | ||||||
|  |     // Since we can't detect the OS level events when a user plugs in an ethernet cable to recheck
 | ||||||
|  |     // what network environment we are in we check our local network address and the gateway to
 | ||||||
|  |     // determine if we need to run the loopback check and router configuration again.
 | ||||||
|  |     var addr = await network.getPrivateIpAsync(); | ||||||
|  |     // Until the author of the `network` package publishes the pull request we gave him
 | ||||||
|  |     // checking the gateway on our units fails because we have the busybox versions of
 | ||||||
|  |     // the linux commands. Gateway is realistically less important than address, so if
 | ||||||
|  |     // we fail in getting it go ahead and use the null value.
 | ||||||
|  |     var gw; | ||||||
|  |     try { | ||||||
|  |       gw = await network.getGatewayIpAsync(); | ||||||
|  |     } catch (err) { | ||||||
|  |       gw = null; | ||||||
|  |     } | ||||||
|  |     if (localAddr === addr && gateway === gw) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var loopResult = await loopback(loopbackDomain); | ||||||
|  |     var notLooped = Object.keys(loopResult.ports).filter(function (port) { | ||||||
|  |       return !loopResult.ports[port]; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // if (notLooped.length) {
 | ||||||
|  |     //   // TODO: try to automatically configure router to forward ports to us.
 | ||||||
|  |     // }
 | ||||||
|  | 
 | ||||||
|  |     // If we are on a public address or all ports we are listening on are forwarded to us then
 | ||||||
|  |     // we don't need the tunnel and we can set the DNS records for all our domains to our public
 | ||||||
|  |     // address. Otherwise we need to use the tunnel to accept traffic. Also since the tunnel will
 | ||||||
|  |     // only be listening on ports 80 and 443 if those are forwarded to us we don't want the tunnel.
 | ||||||
|  |     if (!notLooped.length || (loopResult.ports['80'] && loopResult.ports['443'])) { | ||||||
|  |       if (tunnelActive) { | ||||||
|  |         await disconnectTunnels(); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       if (!tunnelActive) { | ||||||
|  |         await connectAllTunnels(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Don't assign these until the end of the function. This means that if something failed
 | ||||||
|  |     // in the loopback or tunnel connection that we will try to go through the whole process
 | ||||||
|  |     // again next time and hopefully the error is temporary (but if not I'm not sure what the
 | ||||||
|  |     // correct course of action would be anyway).
 | ||||||
|  |     localAddr = addr; | ||||||
|  |     gateway = gw; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var publicAddress; | ||||||
|  |   async function recheckPubAddr() { | ||||||
|  |     await checkNetworkEnv(); | ||||||
|  |     if (tunnelActive) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var addr = await loopback.checkPublicAddr(loopbackDomain); | ||||||
|  |     if (publicAddress === addr) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (conf.debug) { | ||||||
|  |       console.log('previous public address',publicAddress, 'does not match current public address', addr); | ||||||
|  |     } | ||||||
|  |     publicAddress = addr; | ||||||
|  | 
 | ||||||
|  |     await utils.iterateAllModules(function setModuleDNS(mod, domainList) { | ||||||
|  |       if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; } | ||||||
|  | 
 | ||||||
|  |       return utils.getSession(mod.tokenId).then(function (session) { | ||||||
|  |         return dnsCtrl.setDeviceAddress(session, addr, domainList); | ||||||
|  |       }).catch(function (err) { | ||||||
|  |         console.log('error setting DNS records for', domainList.join(', ')); | ||||||
|  |         console.log(err); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function getModuleDiffs(prevConf) { | ||||||
|  |     var prevMods = {}; | ||||||
|  |     var curMods = {}; | ||||||
|  | 
 | ||||||
|  |     // this returns a Promise, but since the functions we use are synchronous
 | ||||||
|  |     // and change our enclosed variables we don't need to wait for the return.
 | ||||||
|  |     utils.iterateAllModules(function (mod, domainList) { | ||||||
|  |       if (mod.type !== 'dns@oauth3.org') { return; } | ||||||
|  | 
 | ||||||
|  |       prevMods[mod.id] = { mod, domainList }; | ||||||
|  |       return true; | ||||||
|  |     }, prevConf); | ||||||
|  |     utils.iterateAllModules(function (mod, domainList) { | ||||||
|  |       if (mod.type !== 'dns@oauth3.org') { return; } | ||||||
|  | 
 | ||||||
|  |       curMods[mod.id] = { mod, domainList }; | ||||||
|  |       return true; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Filter out all of the modules that are exactly the same including domainList
 | ||||||
|  |     // since there is no required action to transition.
 | ||||||
|  |     Object.keys(prevMods).map(function (id) { | ||||||
|  |       if (equal(prevMods[id], curMods[id])) { | ||||||
|  |         delete prevMods[id]; | ||||||
|  |         delete curMods[id]; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return {prevMods, curMods}; | ||||||
|  |   } | ||||||
|  |   async function cleanOldDns(prevConf) { | ||||||
|  |     var {prevMods, curMods} = getModuleDiffs(prevConf); | ||||||
|  | 
 | ||||||
|  |     // Then remove DNS records for the domains that we are no longer responsible for.
 | ||||||
|  |     await Promise.all(Object.values(prevMods).map(function ({mod, domainList}) { | ||||||
|  |       // If the module was disabled before there should be any records that we need to clean up
 | ||||||
|  |       if (mod.disabled) { return; } | ||||||
|  | 
 | ||||||
|  |       var oldDomains; | ||||||
|  |       if (!curMods[mod.id] || curMods[mod.id].disabled || mod.tokenId !== curMods[mod.id].mod.tokenId) { | ||||||
|  |         oldDomains = domainList.slice(); | ||||||
|  |       } else { | ||||||
|  |         oldDomains = domainList.filter(function (domain) { | ||||||
|  |           return curMods[mod.id].domainList.indexOf(domain) < 0; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       if (conf.debug) { | ||||||
|  |         console.log('removing old domains for module', mod.id, oldDomains.join(', ')); | ||||||
|  |       } | ||||||
|  |       if (!oldDomains.length) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return utils.getSession(mod.tokenId).then(function (session) { | ||||||
|  |         return dnsCtrl.removeDomains(session, oldDomains); | ||||||
|  |       }); | ||||||
|  |     }).filter(Boolean)); | ||||||
|  |   } | ||||||
|  |   async function setNewDns(prevConf) { | ||||||
|  |     var {prevMods, curMods} = getModuleDiffs(prevConf); | ||||||
|  | 
 | ||||||
|  |     // And add DNS records for any newly added domains.
 | ||||||
|  |     await Promise.all(Object.values(curMods).map(function ({mod, domainList}) { | ||||||
|  |       // Don't set any new records if the module has been disabled.
 | ||||||
|  |       if (mod.disabled) { return; } | ||||||
|  | 
 | ||||||
|  |       var newDomains; | ||||||
|  |       if (!prevMods[mod.id] || mod.tokenId !== prevMods[mod.id].mod.tokenId) { | ||||||
|  |         newDomains = domainList.slice(); | ||||||
|  |       } else { | ||||||
|  |         newDomains = domainList.filter(function (domain) { | ||||||
|  |           return prevMods[mod.id].domainList.indexOf(domain) < 0; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       if (conf.debug) { | ||||||
|  |         console.log('adding new domains for module', mod.id, newDomains.join(', ')); | ||||||
|  |       } | ||||||
|  |       if (!newDomains.length) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return utils.getSession(mod.tokenId).then(function (session) { | ||||||
|  |         return dnsCtrl.setDeviceAddress(session, publicAddress, newDomains); | ||||||
|  |       }); | ||||||
|  |     }).filter(Boolean)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function check() { | ||||||
|  |     recheckPubAddr().catch(function (err) { | ||||||
|  |       console.error('failed to handle all actions needed for DDNS'); | ||||||
|  |       console.error(err); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   check(); | ||||||
|  |   setInterval(check, 5*60*1000); | ||||||
|  | 
 | ||||||
|  |   var curConf; | ||||||
|  |   function updateConf() { | ||||||
|  |     if (curConf && equal(curConf.ddns, conf.ddns) && equal(curConf.domains, conf.domains)) { | ||||||
|  |       // We could update curConf, but since everything we care about is the same...
 | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!curConf || !equal(curConf.ddns.loopback, conf.ddns.loopback)) { | ||||||
|  |       loopbackDomain = 'oauth3.org'; | ||||||
|  |       if (conf.ddns && conf.ddns.loopback) { | ||||||
|  |         if (conf.ddns.loopback.type === 'tunnel@oauth3.org' && conf.ddns.loopback.domain) { | ||||||
|  |           loopbackDomain = conf.ddns.loopback.domain; | ||||||
|  |         } else { | ||||||
|  |           console.error('invalid loopback configuration: bad type or missing domain'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!curConf) { | ||||||
|  |       // We need to make a deep copy of the config so we can use it next time to
 | ||||||
|  |       // compare and see what setup/cleanup is needed to adapt to the changes.
 | ||||||
|  |       curConf = JSON.parse(JSON.stringify(conf)); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     cleanOldDns(curConf).then(function () { | ||||||
|  |       if (!tunnelActive) { | ||||||
|  |         return setNewDns(curConf); | ||||||
|  |       } | ||||||
|  |       if (equal(curConf.ddns.tunnel, conf.ddns.tunnel)) { | ||||||
|  |         return checkTunnelTokens(); | ||||||
|  |       } else { | ||||||
|  |         return disconnectTunnels().then(connectAllTunnels); | ||||||
|  |       } | ||||||
|  |     }).catch(function (err) { | ||||||
|  |       console.error('error transitioning DNS between configurations'); | ||||||
|  |       console.error(err); | ||||||
|  |     }).then(function () { | ||||||
|  |       // We need to make a deep copy of the config so we can use it next time to
 | ||||||
|  |       // compare and see what setup/cleanup is needed to adapt to the changes.
 | ||||||
|  |       curConf = JSON.parse(JSON.stringify(conf)); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   updateConf(); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     loopbackServer:     loopback.server | ||||||
|  |   , setDeviceAddress:   dnsCtrl.setDeviceAddress | ||||||
|  |   , getDeviceAddresses: dnsCtrl.getDeviceAddresses | ||||||
|  |   , recheckPubAddr:     recheckPubAddr | ||||||
|  |   , updateConf:         updateConf | ||||||
|  |   , challenge | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										116
									
								
								lib/ddns/loopback.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								lib/ddns/loopback.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | |||||||
|  | '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; | ||||||
|  | }; | ||||||
							
								
								
									
										191
									
								
								lib/ddns/tunnel-client-manager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								lib/ddns/tunnel-client-manager.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,191 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (deps, config) { | ||||||
|  |   var stunnel = require('stunnel'); | ||||||
|  |   var jwt = require('jsonwebtoken'); | ||||||
|  |   var activeTunnels = {}; | ||||||
|  |   var activeDomains = {}; | ||||||
|  | 
 | ||||||
|  |   var customNet = { | ||||||
|  |     createConnection: function (opts, cb) { | ||||||
|  |       console.log('[gl.tunnel] creating connection'); | ||||||
|  | 
 | ||||||
|  |       // here "reader" means the socket that looks like the connection being accepted
 | ||||||
|  |       // here "writer" means the remote-looking part of the socket that driving the connection
 | ||||||
|  |       var writer; | ||||||
|  | 
 | ||||||
|  |       function usePair(err, reader) { | ||||||
|  |         if (err) { | ||||||
|  |           process.nextTick(function () { | ||||||
|  |             writer.emit('error', err); | ||||||
|  |           }); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var wrapOpts = Object.assign({localAddress: '127.0.0.2', localPort: 'tunnel-0'}, opts); | ||||||
|  |         wrapOpts.firstChunk = opts.data; | ||||||
|  |         wrapOpts.hyperPeek = !!opts.data; | ||||||
|  | 
 | ||||||
|  |         // Also override the remote and local address info. We use `defineProperty` because
 | ||||||
|  |         // otherwise we run into problems of setting properties with only getters defined.
 | ||||||
|  |         Object.defineProperty(reader, 'remoteAddress', { value: wrapOpts.remoteAddress }); | ||||||
|  |         Object.defineProperty(reader, 'remotePort',    { value: wrapOpts.remotePort }); | ||||||
|  |         Object.defineProperty(reader, 'remoteFamiliy', { value: wrapOpts.remoteFamiliy }); | ||||||
|  |         Object.defineProperty(reader, 'localAddress',  { value: wrapOpts.localAddress }); | ||||||
|  |         Object.defineProperty(reader, 'localPort',     { value: wrapOpts.localPort }); | ||||||
|  |         Object.defineProperty(reader, 'localFamiliy',  { value: wrapOpts.localFamiliy }); | ||||||
|  | 
 | ||||||
|  |         deps.tcp.handler(reader, wrapOpts); | ||||||
|  |         process.nextTick(function () { | ||||||
|  |           // this cb will cause the stream to emit its (actually) first data event
 | ||||||
|  |           // (even though it already gave a peek into that first data chunk)
 | ||||||
|  |           console.log('[tunnel] callback, data should begin to flow'); | ||||||
|  |           cb(); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // We used to use `stream-pair` for non-tls connections, but there are places
 | ||||||
|  |       // that require properties/functions to be present on the socket that aren't
 | ||||||
|  |       // present on a JSStream so it caused problems.
 | ||||||
|  |       writer = require('socket-pair').create(usePair); | ||||||
|  |       return writer; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   function fillData(data) { | ||||||
|  |     if (typeof data === 'string') { | ||||||
|  |       data = { jwt: data }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!data.jwt) { | ||||||
|  |       throw new Error("missing 'jwt' from tunnel data"); | ||||||
|  |     } | ||||||
|  |     var decoded = jwt.decode(data.jwt); | ||||||
|  |     if (!decoded) { | ||||||
|  |       throw new Error('invalid JWT'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!data.tunnelUrl) { | ||||||
|  |       if (!decoded.aud) { | ||||||
|  |         throw new Error('missing tunnelUrl and audience'); | ||||||
|  |       } | ||||||
|  |       data.tunnelUrl = 'wss://' + decoded.aud + '/'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     data.domains = (decoded.domains || []).slice().sort().join(','); | ||||||
|  |     if (!data.domains) { | ||||||
|  |       throw new Error('JWT contains no domains to be forwarded'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return data; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function removeToken(data) { | ||||||
|  |     data = fillData(data); | ||||||
|  | 
 | ||||||
|  |     // Not sure if we might want to throw an error indicating the token didn't
 | ||||||
|  |     // even belong to a  server that existed, but since it never existed we can
 | ||||||
|  |     // consider it as "removed".
 | ||||||
|  |     if (!activeTunnels[data.tunnelUrl]) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     console.log('removing token from tunnel at', data.tunnelUrl); | ||||||
|  |     return activeTunnels[data.tunnelUrl].clear(data.jwt).then(function () { | ||||||
|  |       delete activeDomains[data.domains]; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function addToken(data) { | ||||||
|  |     data = fillData(data); | ||||||
|  | 
 | ||||||
|  |     if (activeDomains[data.domains]) { | ||||||
|  |       // If already have a token with the exact same domains and to the same tunnel
 | ||||||
|  |       // server there isn't really a need to add a new one
 | ||||||
|  |       if (activeDomains[data.domains].tunnelUrl === data.tunnelUrl) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // Otherwise we want to detach from the other tunnel server in favor of the new one
 | ||||||
|  |       console.warn('added token with the exact same domains as another'); | ||||||
|  |       await removeToken(activeDomains[data.domains]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!activeTunnels[data.tunnelUrl]) { | ||||||
|  |       console.log('creating new tunnel client for', data.tunnelUrl); | ||||||
|  |       // We create the tunnel without an initial token so we can append the token and
 | ||||||
|  |       // get the promise that should tell us more about if it worked or not.
 | ||||||
|  |       activeTunnels[data.tunnelUrl] = stunnel.connect({ | ||||||
|  |         stunneld: data.tunnelUrl | ||||||
|  |       , net: customNet | ||||||
|  |         // NOTE: the ports here aren't that important since we are providing a custom
 | ||||||
|  |         // `net.createConnection` that doesn't actually use the port. What is important
 | ||||||
|  |         // is that any services we are interested in are listed in this object and have
 | ||||||
|  |         // a '*' sub-property.
 | ||||||
|  |       , services: { | ||||||
|  |           https: { '*': 443 } | ||||||
|  |         , http:  { '*': 80 } | ||||||
|  |         , smtp:  { '*': 25 } | ||||||
|  |         , smtps: { '*': 587 /*also 465/starttls*/ } | ||||||
|  |         , ssh:   { '*': 22 } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     console.log('appending token to tunnel at', data.tunnelUrl, 'for domains', data.domains); | ||||||
|  |     await activeTunnels[data.tunnelUrl].append(data.jwt); | ||||||
|  | 
 | ||||||
|  |     // Now that we know the tunnel server accepted our token we can save it
 | ||||||
|  |     // to keep record of what domains we are handling and what tunnel server
 | ||||||
|  |     // those domains should go to.
 | ||||||
|  |     activeDomains[data.domains] = data; | ||||||
|  | 
 | ||||||
|  |     // This is mostly for the start, but return the host for the tunnel server
 | ||||||
|  |     // we've connected to (after stripping the protocol and path away).
 | ||||||
|  |     return data.tunnelUrl.replace(/^[a-z]*:\/\//i, '').replace(/\/.*/, ''); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function acquireToken(session, domains) { | ||||||
|  |     var OAUTH3 = deps.OAUTH3; | ||||||
|  | 
 | ||||||
|  |     // The OAUTH3 library stores some things on the root session object that we usually
 | ||||||
|  |     // just leave inside the token, but we need to pull those out before we use it here
 | ||||||
|  |     session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss; | ||||||
|  |     session.client_uri = session.client_uri || session.token.azp; | ||||||
|  |     session.scope = session.scope || session.token.scp; | ||||||
|  | 
 | ||||||
|  |     console.log('asking for tunnel token from', session.token.aud); | ||||||
|  |     var opts = { | ||||||
|  |       api: 'tunnel.token' | ||||||
|  |     , session: session | ||||||
|  |     , data: { | ||||||
|  |         domains: domains | ||||||
|  |       , device: { | ||||||
|  |           hostname: config.device.hostname | ||||||
|  |         , id: config.device.uid || config.device.id | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     var directives = await OAUTH3.discover(session.token.aud); | ||||||
|  |     var tokenData = await OAUTH3.api(directives.api, opts); | ||||||
|  |     return addToken(tokenData); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function disconnectAll() { | ||||||
|  |     Object.keys(activeTunnels).forEach(function (key) { | ||||||
|  |       activeTunnels[key].end(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function currentTokens() { | ||||||
|  |     return JSON.parse(JSON.stringify(activeDomains)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     start:       acquireToken | ||||||
|  |   , startDirect: addToken | ||||||
|  |   , remove:      removeToken | ||||||
|  |   , disconnect:  disconnectAll | ||||||
|  |   , current:     currentTokens | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										102
									
								
								lib/ddns/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								lib/ddns/utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,102 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (deps, conf) { | ||||||
|  | 
 | ||||||
|  |   async function getSession(id) { | ||||||
|  |     var session = await deps.storage.tokens.get(id); | ||||||
|  |     if (!session) { | ||||||
|  |       throw new Error('no user token with ID "' + id + '"'); | ||||||
|  |     } | ||||||
|  |     return session; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function iterateAllModules(action, curConf) { | ||||||
|  |     curConf = curConf || conf; | ||||||
|  |     var promises = []; | ||||||
|  | 
 | ||||||
|  |     curConf.domains.forEach(function (dom) { | ||||||
|  |       if (!dom.modules || !Array.isArray(dom.modules.ddns) || !dom.modules.ddns.length) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // For the time being all of our things should only be tried once (regardless if it succeeded)
 | ||||||
|  |       // TODO: revisit this behavior when we support multiple ways of setting records, and/or
 | ||||||
|  |       // if we want to allow later modules to run if early modules fail.
 | ||||||
|  |       promises.push(dom.modules.ddns.reduce(function (prom, mod) { | ||||||
|  |         if (prom) { return prom; } | ||||||
|  |         return action(mod, dom.names); | ||||||
|  |       }, null)); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     curConf.ddns.modules.forEach(function (mod) { | ||||||
|  |       promises.push(action(mod, mod.domains)); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return Promise.all(promises.filter(Boolean)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var tldCache = {}; | ||||||
|  |   async function updateTldCache(provider) { | ||||||
|  |     var reqObj = { | ||||||
|  |       url: deps.OAUTH3.url.normalize(provider) + '/api/com.daplie.domains/prices' | ||||||
|  |     , method: 'GET' | ||||||
|  |     , json: true | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     var resp = await deps.OAUTH3.request(reqObj); | ||||||
|  |     var tldObj = {}; | ||||||
|  |     resp.data.forEach(function (tldInfo) { | ||||||
|  |       if (tldInfo.enabled) { | ||||||
|  |         tldObj[tldInfo.tld] = true; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     tldCache[provider] = { | ||||||
|  |       time: Date.now() | ||||||
|  |     , tlds: tldObj | ||||||
|  |     }; | ||||||
|  |     return tldObj; | ||||||
|  |   } | ||||||
|  |   async function getTlds(provider) { | ||||||
|  |     // If we've never cached the results we need to return the promise that will fetch the result,
 | ||||||
|  |     // otherwise we can return the cached value. If the cached value has "expired", we can still
 | ||||||
|  |     // return the cached value we just want to update the cache in parellel (making sure we only
 | ||||||
|  |     // update once).
 | ||||||
|  |     if (!tldCache[provider]) { | ||||||
|  |       tldCache[provider] = { | ||||||
|  |         updating: true | ||||||
|  |       , tlds: updateTldCache(provider) | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (!tldCache[provider].updating && Date.now() - tldCache[provider].time > 24 * 60 * 60 * 1000) { | ||||||
|  |       tldCache[provider].updating = true; | ||||||
|  |       updateTldCache(provider); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return tldCache[provider].tlds; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function splitDomains(provider, domains) { | ||||||
|  |     var tlds = await getTlds(provider); | ||||||
|  |     return domains.map(function (domain) { | ||||||
|  |       var split = domain.split('.'); | ||||||
|  |       var tldSegCnt = tlds[split.slice(-2).join('.')] ? 2 : 1; | ||||||
|  | 
 | ||||||
|  |       // Currently assuming that the sld can't contain dots, and that the tld can have at
 | ||||||
|  |       // most one dot. Not 100% sure this is a valid assumption, but exceptions should be
 | ||||||
|  |       // rare even if the assumption isn't valid.
 | ||||||
|  |       return { | ||||||
|  |         tld: split.slice(-tldSegCnt).join('.') | ||||||
|  |       , sld: split.slice(-tldSegCnt - 1, -tldSegCnt).join('.') | ||||||
|  |       , sub: split.slice(0, -tldSegCnt - 1).join('.') | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     getSession | ||||||
|  |   , iterateAllModules | ||||||
|  |   , getTlds | ||||||
|  |   , splitDomains | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										30
									
								
								lib/domain-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/domain-utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | '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
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								lib/match-ips.js
									
									
									
									
									
								
							| @ -1,117 +0,0 @@ | |||||||
| '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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								lib/mdns.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,203 @@ | |||||||
|  | '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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								lib/servers.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,179 @@ | |||||||
|  | '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 | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										91
									
								
								lib/socks5-server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								lib/socks5-server.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | |||||||
|  | '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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								lib/storage.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,225 @@ | |||||||
|  | '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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										543
									
								
								lib/tcp/http.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,543 @@ | |||||||
|  | '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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								lib/tcp/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,242 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (deps, config) { | ||||||
|  |   console.log('config', config); | ||||||
|  | 
 | ||||||
|  |   var listeners = require('../servers').listeners.tcp; | ||||||
|  |   var domainUtils = require('../domain-utils'); | ||||||
|  |   var modules; | ||||||
|  | 
 | ||||||
|  |   var addrProperties = [ | ||||||
|  |     'remoteAddress' | ||||||
|  |   , 'remotePort' | ||||||
|  |   , 'remoteFamily' | ||||||
|  |   , 'localAddress' | ||||||
|  |   , 'localPort' | ||||||
|  |   , 'localFamily' | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   function nameMatchesDomains(name, domainList) { | ||||||
|  |     return domainList.some(function (pattern) { | ||||||
|  |       return domainUtils.match(pattern, name); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function proxy(mod, conn, opts) { | ||||||
|  |     // First thing we need to add to the connection options is where to proxy the connection to
 | ||||||
|  |     var newConnOpts = domainUtils.separatePort(mod.address || ''); | ||||||
|  |     newConnOpts.port = newConnOpts.port || mod.port; | ||||||
|  |     newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; | ||||||
|  | 
 | ||||||
|  |     // Then we add all of the connection address information. We need to prefix all of the
 | ||||||
|  |     // properties with '_' so we can provide the information to any connection `createConnection`
 | ||||||
|  |     // implementation but not have the default implementation try to bind the same local port.
 | ||||||
|  |     addrProperties.forEach(function (name) { | ||||||
|  |       newConnOpts['_' + name] = opts[name] || opts['_'+name] || conn[name] || conn['_'+name]; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     modules.proxy(conn, newConnOpts); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function checkTcpProxy(conn, opts) { | ||||||
|  |     var proxied = false; | ||||||
|  | 
 | ||||||
|  |     // TCP Proxying (ie routing based on domain name [vs local port]) only works for
 | ||||||
|  |     // TLS wrapped connections, so if the opts don't give us a servername or don't tell us
 | ||||||
|  |     // this is the decrypted side of a TLS connection we can't handle it here.
 | ||||||
|  |     if (!opts.servername || !opts.encrypted) { return proxied; } | ||||||
|  | 
 | ||||||
|  |     proxied = config.domains.some(function (dom) { | ||||||
|  |       if (!dom.modules || !Array.isArray(dom.modules.tcp)) { return false; } | ||||||
|  |       if (!nameMatchesDomains(opts.servername, dom.names)) { return false; } | ||||||
|  | 
 | ||||||
|  |       return dom.modules.tcp.some(function (mod) { | ||||||
|  |         if (mod.type !== 'proxy') { return false; } | ||||||
|  | 
 | ||||||
|  |         return proxy(mod, conn, opts); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     proxied = proxied || config.tcp.modules.some(function (mod) { | ||||||
|  |       if (mod.type !== 'proxy') { return false; } | ||||||
|  |       if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; } | ||||||
|  | 
 | ||||||
|  |       return proxy(mod, conn, opts); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return proxied; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function checkTcpForward(conn, opts) { | ||||||
|  |     // TCP forwarding (ie routing connections based on local port) requires the local port
 | ||||||
|  |     if (!conn.localPort) { return false; } | ||||||
|  | 
 | ||||||
|  |     return config.tcp.modules.some(function (mod) { | ||||||
|  |       if (mod.type !== 'forward')                { return false; } | ||||||
|  |       if (mod.ports.indexOf(conn.localPort) < 0) { return false; } | ||||||
|  | 
 | ||||||
|  |       return proxy(mod, conn, opts); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
 | ||||||
|  |   function peek(conn, firstChunk, opts) { | ||||||
|  |     opts.firstChunk = firstChunk; | ||||||
|  |     conn.__opts = opts; | ||||||
|  |     // TODO port/service-based routing can do here
 | ||||||
|  | 
 | ||||||
|  |     // TLS byte 1 is handshake and byte 6 is client hello
 | ||||||
|  |     if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) { | ||||||
|  |       modules.tls.emit('connection', conn); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // This doesn't work with TLS, but now that we know this isn't a TLS connection we can
 | ||||||
|  |     // unshift the first chunk back onto the connection for future use. The unshift should
 | ||||||
|  |     // happen after any listeners are attached to it but before any new data comes in.
 | ||||||
|  |     if (!opts.hyperPeek) { | ||||||
|  |       process.nextTick(function () { | ||||||
|  |         conn.unshift(firstChunk); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Connection is not TLS, check for HTTP next.
 | ||||||
|  |     if (firstChunk[0] > 32 && firstChunk[0] < 127) { | ||||||
|  |       var firstStr = firstChunk.toString(); | ||||||
|  |       if (/HTTP\//i.test(firstStr)) { | ||||||
|  |         modules.http.emit('connection', conn); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     console.warn('failed to identify protocol from first chunk', firstChunk); | ||||||
|  |     conn.destroy(); | ||||||
|  |   } | ||||||
|  |   function tcpHandler(conn, opts) { | ||||||
|  |     function getProp(name) { | ||||||
|  |       return opts[name] || opts['_'+name] || conn[name] || conn['_'+name]; | ||||||
|  |     } | ||||||
|  |     opts = opts || {}; | ||||||
|  |     var logName = getProp('remoteAddress') + ':' + getProp('remotePort') + ' -> ' + | ||||||
|  |                   getProp('localAddress')  + ':' + getProp('localPort'); | ||||||
|  |     console.log('[tcpHandler]', logName, 'connection started - encrypted: ' + (opts.encrypted || false)); | ||||||
|  | 
 | ||||||
|  |     var start = Date.now(); | ||||||
|  |     conn.on('timeout', function () { | ||||||
|  |       console.log('[tcpHandler]', logName, 'connection timed out', (Date.now()-start)/1000); | ||||||
|  |     }); | ||||||
|  |     conn.on('end', function () { | ||||||
|  |       console.log('[tcpHandler]', logName, 'connection ended', (Date.now()-start)/1000); | ||||||
|  |     }); | ||||||
|  |     conn.on('close', function () { | ||||||
|  |       console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (checkTcpForward(conn, opts)) { return; } | ||||||
|  |     if (checkTcpProxy(conn, opts))   { return; } | ||||||
|  | 
 | ||||||
|  |     // XXX PEEK COMMENT XXX
 | ||||||
|  |     // TODO we can have our cake and eat it too
 | ||||||
|  |     // we can skip the need to wrap the TLS connection twice
 | ||||||
|  |     // because we've already peeked at the data,
 | ||||||
|  |     // but this needs to be handled better before we enable that
 | ||||||
|  |     // (because it creates new edge cases)
 | ||||||
|  |     if (opts.hyperPeek) { | ||||||
|  |       console.log('hyperpeek'); | ||||||
|  |       peek(conn, opts.firstChunk, opts); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function onError(err) { | ||||||
|  |       console.error('[error] socket errored peeking -', err); | ||||||
|  |       conn.destroy(); | ||||||
|  |     } | ||||||
|  |     conn.once('error', onError); | ||||||
|  |     conn.once('data', function (chunk) { | ||||||
|  |       conn.removeListener('error', onError); | ||||||
|  |       peek(conn, chunk, opts); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   process.nextTick(function () { | ||||||
|  |     modules = {}; | ||||||
|  |     modules.tcpHandler = tcpHandler; | ||||||
|  |     modules.proxy = require('./proxy-conn').create(deps, config); | ||||||
|  |     modules.tls   = require('./tls').create(deps, config, modules); | ||||||
|  |     modules.http  = require('./http').create(deps, config, modules); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   function updateListeners() { | ||||||
|  |     var current = listeners.list(); | ||||||
|  |     var wanted = config.tcp.bind; | ||||||
|  | 
 | ||||||
|  |     if (!Array.isArray(wanted)) { wanted = []; } | ||||||
|  |     wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356); | ||||||
|  | 
 | ||||||
|  |     var closeProms = current.filter(function (port) { | ||||||
|  |       return wanted.indexOf(port) < 0; | ||||||
|  |     }).map(function (port) { | ||||||
|  |       return listeners.close(port, 1000); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // We don't really need to filter here since listening on the same port with the
 | ||||||
|  |     // same handler function twice is basically a no-op.
 | ||||||
|  |     var openProms = wanted.map(function (port) { | ||||||
|  |       return listeners.add(port, tcpHandler); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return Promise.all(closeProms.concat(openProms)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var mainPort; | ||||||
|  |   function updateConf() { | ||||||
|  |     updateListeners().catch(function (err) { | ||||||
|  |       console.error('Error updating TCP listeners to match bind configuration'); | ||||||
|  |       console.error(err); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     var unforwarded = {}; | ||||||
|  |     config.tcp.bind.forEach(function (port) { | ||||||
|  |       unforwarded[port] = true; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     config.tcp.modules.forEach(function (mod) { | ||||||
|  |       if (['forward', 'proxy'].indexOf(mod.type) < 0) { | ||||||
|  |         console.warn('unknown TCP module type specified', JSON.stringify(mod)); | ||||||
|  |       } | ||||||
|  |       if (mod.type !== 'forward') { return; } | ||||||
|  | 
 | ||||||
|  |       mod.ports.forEach(function (port) { | ||||||
|  |         if (!unforwarded[port]) { | ||||||
|  |           console.warn('trying to forward TCP port ' + port + ' multiple times or it is unbound'); | ||||||
|  |         } else { | ||||||
|  |           delete unforwarded[port]; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Not really sure what we can reasonably do to prevent this. At least not without making
 | ||||||
|  |     // our configuration validation more complicated.
 | ||||||
|  |     if (!Object.keys(unforwarded).length) { | ||||||
|  |       console.warn('no bound TCP ports are not being forwarded, admin interface will be inaccessible'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // If we are listening on port 443 make that the main port we respond to mDNS queries with
 | ||||||
|  |     // otherwise choose the lowest number port we are bound to but not forwarding.
 | ||||||
|  |     if (unforwarded['443']) { | ||||||
|  |       mainPort = 443; | ||||||
|  |     } else { | ||||||
|  |       mainPort = Object.keys(unforwarded).map(Number).sort((a, b) => a - b)[0]; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   updateConf(); | ||||||
|  | 
 | ||||||
|  |   var result =  { | ||||||
|  |     updateConf | ||||||
|  |   , handler: tcpHandler | ||||||
|  |   }; | ||||||
|  |   Object.defineProperty(result, 'mainPort', {enumerable: true, get: () => mainPort}); | ||||||
|  | 
 | ||||||
|  |   return result; | ||||||
|  | }; | ||||||
							
								
								
									
										81
									
								
								lib/tcp/proxy-conn.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								lib/tcp/proxy-conn.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | '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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								lib/tcp/tls.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,349 @@ | |||||||
|  | '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() | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										131
									
								
								lib/tunnel-server-manager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								lib/tunnel-server-manager.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | |||||||
|  | '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
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								lib/tunnel.js
									
									
									
									
									
								
							| @ -1,144 +0,0 @@ | |||||||
| '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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								lib/udp.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (deps, config) { | ||||||
|  |   var listeners = require('./servers').listeners.udp; | ||||||
|  | 
 | ||||||
|  |   function packetHandler(port, msg) { | ||||||
|  |     if (!Array.isArray(config.udp.modules)) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var socket = require('dgram').createSocket('udp4'); | ||||||
|  |     config.udp.modules.forEach(function (mod) { | ||||||
|  |       if (mod.type !== 'forward') { | ||||||
|  |         // To avoid logging bad modules every time we get a UDP packet we assign a warned
 | ||||||
|  |         // property to the module (non-enumerable so it won't be saved to the config or
 | ||||||
|  |         // show up in the API).
 | ||||||
|  |         if (!mod.warned) { | ||||||
|  |           console.warn('found bad DNS module', mod); | ||||||
|  |           Object.defineProperty(mod, 'warned', {value: true, enumerable: false}); | ||||||
|  |         } | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       if (mod.ports.indexOf(port) < 0) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       var dest = require('./domain-utils').separatePort(mod.address || ''); | ||||||
|  |       dest.port = dest.port || mod.port; | ||||||
|  |       dest.host = dest.host || mod.host || 'localhost'; | ||||||
|  |       socket.send(msg, dest.port, dest.host); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function updateListeners() { | ||||||
|  |     var current = listeners.list(); | ||||||
|  |     var wanted = config.udp.bind; | ||||||
|  | 
 | ||||||
|  |     if (!Array.isArray(wanted)) { wanted = []; } | ||||||
|  |     wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356); | ||||||
|  | 
 | ||||||
|  |     current.forEach(function (port) { | ||||||
|  |       if (wanted.indexOf(port) < 0) { | ||||||
|  |         listeners.close(port); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     wanted.forEach(function (port) { | ||||||
|  |       if (current.indexOf(port) < 0) { | ||||||
|  |         listeners.add(port, packetHandler.bind(port)); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   updateListeners(); | ||||||
|  |   return { | ||||||
|  |     updateConf: updateListeners | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										64
									
								
								lib/worker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								lib/worker.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var config; | ||||||
|  | var modules; | ||||||
|  | 
 | ||||||
|  | // Everything that uses the config should be reading it when relevant rather than
 | ||||||
|  | // just at the beginning, so we keep the reference for the main object and just
 | ||||||
|  | // change all of its properties to match the new config.
 | ||||||
|  | function update(conf) { | ||||||
|  |   var newKeys = Object.keys(conf); | ||||||
|  | 
 | ||||||
|  |   Object.keys(config).forEach(function (key) { | ||||||
|  |     if (newKeys.indexOf(key) < 0) { | ||||||
|  |       delete config[key]; | ||||||
|  |     } else { | ||||||
|  |       config[key] = conf[key]; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   console.log('config update', JSON.stringify(config)); | ||||||
|  |   Object.values(modules).forEach(function (mod) { | ||||||
|  |     if (typeof mod.updateConf === 'function') { | ||||||
|  |       mod.updateConf(config); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function create(conf) { | ||||||
|  |   var PromiseA = require('bluebird'); | ||||||
|  |   var OAUTH3 = require('../packages/assets/org.oauth3'); | ||||||
|  |   require('../packages/assets/org.oauth3/oauth3.domains.js'); | ||||||
|  |   require('../packages/assets/org.oauth3/oauth3.dns.js'); | ||||||
|  |   require('../packages/assets/org.oauth3/oauth3.tunnel.js'); | ||||||
|  |   OAUTH3._hooks = require('../packages/assets/org.oauth3/oauth3.node.storage.js'); | ||||||
|  | 
 | ||||||
|  |   config = conf; | ||||||
|  |   var deps = { | ||||||
|  |     messenger: process | ||||||
|  |   , PromiseA: PromiseA | ||||||
|  |   , OAUTH3: OAUTH3 | ||||||
|  |   , request: PromiseA.promisify(require('request')) | ||||||
|  |   , recase: require('recase').create({}) | ||||||
|  |     // Note that if a custom createConnections is used it will be called with different
 | ||||||
|  |     // sets of custom options based on what is actually being proxied. Most notably the
 | ||||||
|  |     // HTTP proxying connection creation is not something we currently control.
 | ||||||
|  |   , net: require('net') | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   modules = { | ||||||
|  |     storage:  require('./storage').create(deps, conf) | ||||||
|  |   , socks5:   require('./socks5-server').create(deps, conf) | ||||||
|  |   , ddns:     require('./ddns').create(deps, conf) | ||||||
|  |   , mdns:     require('./mdns').create(deps, conf) | ||||||
|  |   , udp:      require('./udp').create(deps, conf) | ||||||
|  |   , tcp:      require('./tcp').create(deps, conf) | ||||||
|  |   , stunneld: require('./tunnel-server-manager').create(deps, config) | ||||||
|  |   }; | ||||||
|  |   Object.assign(deps, modules); | ||||||
|  | 
 | ||||||
|  |   process.removeListener('message', create); | ||||||
|  |   process.on('message', update); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | process.on('message', create); | ||||||
							
								
								
									
										58
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								package.json
									
									
									
									
									
								
							| @ -1,14 +1,14 @@ | |||||||
| { | { | ||||||
|   "name": "goldilocks", |   "name": "goldilocks", | ||||||
|   "version": "2.2.0", |   "version": "1.1.6", | ||||||
|   "description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.", |   "description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.", | ||||||
|   "main": "bin/goldilocks.js", |   "main": "bin/goldilocks.js", | ||||||
|   "repository": { |   "repository": { | ||||||
|     "type": "git", |     "type": "git", | ||||||
|     "url": "git@git.daplie.com:Daplie/goldilocks.js.git" |     "url": "git.coolaj86.com:coolaj86/goldilocks.js.git" | ||||||
|   }, |   }, | ||||||
|   "author": "AJ ONeal <aj@daplie.com> (https://daplie.com/)", |   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", | ||||||
|   "license": "SEE LICENSE IN LICENSE.txt", |   "license": "(MIT OR Apache-2.0)", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "test": "node bin/goldilocks.js -p 8443 -d /tmp/" |     "test": "node bin/goldilocks.js -p 8443 -d /tmp/" | ||||||
|   }, |   }, | ||||||
| @ -34,34 +34,42 @@ | |||||||
|     "server" |     "server" | ||||||
|   ], |   ], | ||||||
|   "bugs": { |   "bugs": { | ||||||
|     "url": "https://git.daplie.com/Daplie/server-https/issues" |     "url": "https://git.coolaj86.com/coolaj86/goldilocks.js/issues" | ||||||
|   }, |   }, | ||||||
|   "homepage": "https://git.daplie.com/Daplie/goldilocks.js#readme", |   "homepage": "https://git.coolaj86.com/coolaj86/goldilocks.js", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "bluebird": "^3.4.6", |     "bluebird": "^3.4.6", | ||||||
|     "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", |     "body-parser": "1", | ||||||
|     "daplie-tunnel": "git+https://git.daplie.com/Daplie/daplie-cli-tunnel.git#master", |     "commander": "^2.9.0", | ||||||
|     "ddns-cli": "git+https://git.daplie.com/Daplie/node-ddns-client.git#master", |     "deep-equal": "^1.0.1", | ||||||
|     "express": "git+https://github.com/expressjs/express.git#4.x", |     "dns-suite": "1", | ||||||
|  |     "express": "4", | ||||||
|     "finalhandler": "^0.4.0", |     "finalhandler": "^0.4.0", | ||||||
|     "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", |     "greenlock": "2.1", | ||||||
|     "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master", |     "http-proxy": "^1.16.2", | ||||||
|     "httpolyglot": "^0.1.1", |     "human-readable-ids": "1", | ||||||
|     "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", |     "ipaddr.js": "v1.3", | ||||||
|     "ipify": "^1.1.0", |     "js-yaml": "^3.8.3", | ||||||
|     "js-yaml": "^3.8.1", |     "jsonschema": "^1.2.0", | ||||||
|     "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", |     "jsonwebtoken": "^7.4.0", | ||||||
|     "le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master", |     "le-challenge-fs": "2", | ||||||
|     "le-challenge-sni": "^2.0.1", |     "le-challenge-sni": "^2.0.1", | ||||||
|     "livereload": "^0.6.0", |     "le-store-certbot": "2", | ||||||
|     "localhost.daplie.me-certificates": "^1.3.0", |     "localhost.daplie.me-certificates": "^1.3.5", | ||||||
|     "minimist": "^1.1.1", |     "network": "^0.4.0", | ||||||
|     "oauth3-cli": "git+https://git.daplie.com/OAuth3/oauth3-cli.git#master", |     "recase": "v1.0.4", | ||||||
|     "recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4", |  | ||||||
|     "redirect-https": "^1.1.0", |     "redirect-https": "^1.1.0", | ||||||
|     "scmp": "git+https://github.com/freewil/scmp.git#1.x", |     "request": "^2.81.0", | ||||||
|  |     "scmp": "1", | ||||||
|     "serve-index": "^1.7.0", |     "serve-index": "^1.7.0", | ||||||
|     "serve-static": "^1.10.0", |     "serve-static": "^1.10.0", | ||||||
|     "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" |     "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" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,185 +0,0 @@ | |||||||
| '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).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 } })); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , 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 |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,23 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var api = require('./index.js').api; |  | ||||||
| var OAUTH3 = require('../../assets/org.oauth3/'); |  | ||||||
| // these all auto-register
 |  | ||||||
| require('../../assets/org.oauth3/oauth3.domains.js'); |  | ||||||
| require('../../assets/org.oauth3/oauth3.dns.js'); |  | ||||||
| require('../../assets/org.oauth3/oauth3.tunnel.js'); |  | ||||||
| OAUTH3._hooks = require('../../assets/org.oauth3/oauth3.node.storage.js'); |  | ||||||
| 
 |  | ||||||
| api.tunnel( |  | ||||||
|   { |  | ||||||
|     OAUTH3: OAUTH3 |  | ||||||
|   , options: { |  | ||||||
|       device: { |  | ||||||
|         hostname: 'test.local' |  | ||||||
|       , id: '' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   // OAUTH3.hooks.session.get('oauth3.org').then(function (result) { console.log(result) });
 |  | ||||||
| , require('./test.session.json') |  | ||||||
| ); |  | ||||||
| @ -1 +0,0 @@ | |||||||
| Subproject commit 3a805d071a4a84371b9bc674839d2511dd9aa4d3 |  | ||||||
| @ -1,23 +0,0 @@ | |||||||
| '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
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								terms.sh
									
									
									
									
									
								
							| @ -1,3 +0,0 @@ | |||||||
| # adding TOS to TXT DNS Record |  | ||||||
| daplie dns:set -n _terms._cloud.localhost.foo.daplie.me -t TXT -a '{"url":"oauth3.org/tos/draft","explicit":true}' --ttl 3600 |  | ||||||
| daplie dns:set -n _terms._cloud.localhost.alpha.daplie.me -t TXT -a '{"url":"oauth3.org/tos/draft","explicit":true}' --ttl 3600 |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
| 
 |  | ||||||
| node serve.js \ |  | ||||||
|   --port 8443 \ |  | ||||||
|   --key node_modules/localhost.daplie.me-certificates/privkey.pem \ |  | ||||||
|   --cert node_modules/localhost.daplie.me-certificates/fullchain.pem \ |  | ||||||
|   --root node_modules/localhost.daplie.me-certificates/root.pem \ |  | ||||||
|   -c "$(cat node_modules/localhost.daplie.me-certificates/root.pem)" & |  | ||||||
| 
 |  | ||||||
| PID=$! |  | ||||||
| 
 |  | ||||||
| sleep 1 |  | ||||||
| curl -s --insecure http://localhost.daplie.me:8443 > ./root.pem |  | ||||||
| curl -s https://localhost.daplie.me:8443 --cacert ./root.pem |  | ||||||
| 
 |  | ||||||
| rm ./root.pem |  | ||||||
| kill $PID 2>/dev/null |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| 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