Compare commits
No commits in common. "59088716336375dcc08387b8cf7ff179e495d57a" and "f4e228431279e1b7b89860c17d15357c2159e67d" have entirely different histories.
5908871633
...
f4e2284312
|
@ -1,19 +1,4 @@
|
|||
node_modules.*
|
||||
*.*.sw*
|
||||
etc/acme/
|
||||
bin/node
|
||||
bin/npm
|
||||
bin/npx
|
||||
bin/telebit
|
||||
bin/telebitd
|
||||
bin/telebit_uninstall
|
||||
usr/share/dist/Library/LaunchDaemons/cloud.telebit.remote.plist
|
||||
usr/share/dist/etc/skel/Library/LaunchAgents/cloud.telebit.remote.plist
|
||||
usr/share/dist/etc/systemd/system/telebit.service
|
||||
usr/share/dist/etc/skel/.config/systemd/user/telebit.service
|
||||
./etc/
|
||||
./include/
|
||||
./share/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
206
LICENSE
206
LICENSE
|
@ -1,38 +1,198 @@
|
|||
Copyright 2016 AJ ONeal
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
This is open source software; you can redistribute it and/or modify it under the
|
||||
terms of either:
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
a) the "MIT License"
|
||||
b) the "Apache-2.0 License"
|
||||
1. Definitions.
|
||||
|
||||
MIT License
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
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:
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
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.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
Apache-2.0 License Summary
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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
|
||||
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,
|
||||
|
|
465
README.md
465
README.md
|
@ -1,354 +1,98 @@
|
|||
# Telebit™ Remote
|
||||
# Telebit™
|
||||
|
||||
Because friends don't let friends localhost™
|
||||
A client that works in combination with [stunneld.js](https://git.coolaj86.com/coolaj86/tunnel-server.js)
|
||||
to allow you to serve http and https from any computer, anywhere through a secure tunnel.
|
||||
|
||||
| Sponsored by [ppl](https://ppl.family)
|
||||
| **Telebit Remote**
|
||||
| [Telebit Relay](https://git.coolaj86.com/coolaj86/telebit-relay.js)
|
||||
|
|
||||
|
||||
Break out of localhost.
|
||||
=======
|
||||
|
||||
If you need to get bits from here to there, Telebit gets the job done.
|
||||
|
||||
Install Telebit Remote on any device - your laptop, raspberry pi, whatever -
|
||||
and now you can access that device from anywhere, even securely in a web browser.
|
||||
|
||||
How does it work?
|
||||
It's a net server that uses a relay to allow multiplexed incoming connections
|
||||
on any external port.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* [x] Show your mom the web app you're working on
|
||||
* [x] Access your Raspberry Pi from behind a firewall
|
||||
* [x] Watch Netflix without region restrictions while traveling
|
||||
* [x] SSH over HTTPS on networks with restricted ports or protocols
|
||||
* [x] Access your wife's laptop while she's on a flight
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
You do this:
|
||||
|
||||
curl -fsSL https://get.telebit.cloud | bash
|
||||
|
||||
You get this:
|
||||
|
||||
~/telebit http 3000
|
||||
> Forwarding lucky-duck-42.telebit.cloud => localhost:3000
|
||||
|
||||
~/telebit http ~/sites/example.com/
|
||||
> Serving ~/sites/example.com/ as lucky-duck-42.telebit.cloud
|
||||
|
||||
And this:
|
||||
|
||||
~/telebit tcp 5050
|
||||
> Forwarding telebit.cloud:1337 => localhost:5050
|
||||
|
||||
And even this:
|
||||
|
||||
~/telebit ssh auto
|
||||
> Forwarding ssh telebit.cloud -p 1337 => localhost:22
|
||||
> Forwarding ssh+https (openssl proxy) => localhost:22
|
||||
|
||||
No privileged ports. No sudo. End-to-end encryption.
|
||||
|
||||
Fastest way to test a site, share a file, and pair over ssh.
|
||||
|
||||
Install
|
||||
=======
|
||||
|
||||
Mac & Linux
|
||||
-----------
|
||||
|
||||
Open Terminal and run this install script:
|
||||
|
||||
```
|
||||
curl -fsSL https://get.telebit.cloud | bash
|
||||
```
|
||||
|
||||
<!--
|
||||
```
|
||||
bash <( curl -fsSL https://get.telebit.cloud )
|
||||
```
|
||||
|
||||
<small>
|
||||
Note: **fish**, **zsh**, and other **non-bash** users should do this
|
||||
|
||||
```
|
||||
curl -fsSL https://get.telebit.cloud/ > get.sh; bash get.sh
|
||||
```
|
||||
</small>
|
||||
-->
|
||||
|
||||
What does the installer do?
|
||||
|
||||
* install Telebit Remote to `~/Applications/telebit/`
|
||||
* symlink the executable to `~/telebit` for convenience
|
||||
* create the appropriate system launcher file
|
||||
* `/etc/systemd/system/telebit.service`
|
||||
* `~/Library/LaunchAgents/cloud.telebit.remote.plist`
|
||||
* create local user config
|
||||
* `~/.config/telebit/telebit.yml`
|
||||
* `~/.local/share/telebit`
|
||||
|
||||
Of course, feel free to inspect it before you run it: `curl -fsSL https://get.telebit.cloud`
|
||||
|
||||
**You can customize the installation**:
|
||||
|
||||
```bash
|
||||
export NODEJS_VER=v10.2 # v10.2 is tested working, but we can test other versions
|
||||
export TELEBIT_VERSION=master # git tag or branch to install from
|
||||
export TELEBIT_USERSPACE=no # install as a system service (launchd, systemd only)
|
||||
export TELEBIT_PATH=/opt/telebit
|
||||
export TELEBIT_USER=telebit
|
||||
export TELEBIT_GROUP=telebit
|
||||
curl -fsSL https://get.telebit.cloud/ | bash
|
||||
```
|
||||
|
||||
That will change the bundled version of node.js is bundled with Telebit Relay
|
||||
and the path to which Telebit Relay installs.
|
||||
|
||||
Windows & Node.js
|
||||
-----------------
|
||||
|
||||
1. Install [node.js](https://nodejs.org)
|
||||
2. Open _Node.js_
|
||||
2. Run the command `npm install -g telebit`
|
||||
2. Copy the example daemon conifg to your user folder `.config/telebit/telebitd.yml` (such as `/Users/John/.config/telebit/telebitd.yml`)
|
||||
2. Copy the example remote conifg to your user folder `.config/telebit/telebit.yml` (such as `/Users/John/.config/telebit/telebit.yml`)
|
||||
2. Change the email address
|
||||
2. Run `npx telebit init` and follow the instructions
|
||||
2. Run `npx telebit list`
|
||||
|
||||
**Note**: Use node.js **v10.2.1**
|
||||
|
||||
(there are specific bugs in each of
|
||||
v8.x,
|
||||
[v9.x](https://github.com/nodejs/node/issues/20241),
|
||||
v10.0,
|
||||
and v10.3
|
||||
that each cause telebit to crash)
|
||||
|
||||
Remote Usage
|
||||
============
|
||||
|
||||
```
|
||||
# commands
|
||||
telebit <command>
|
||||
|
||||
# domain and port control
|
||||
telebit <service> <handler> [servername] [options ...]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
telebit status # whether enabled or disabled
|
||||
telebit enable # disallow incoming connections
|
||||
telebit disable # allow incoming connections
|
||||
telebit restart # kill daemon and allow system launcher to restart it
|
||||
|
||||
telebit list # list rules for servernames and ports
|
||||
|
||||
################
|
||||
# HTTP #
|
||||
################
|
||||
|
||||
telebit http <handler> [servername] [opts]
|
||||
|
||||
telebit http none # remove all https handlers
|
||||
telebit http 3000 # forward all https traffic to port 3000
|
||||
telebit http /module/path # load a node module to handle all https traffic
|
||||
|
||||
telebit http none example.com # remove https handler from example.com
|
||||
telebit http 3001 example.com # forward https traffic for example.com to port 3001
|
||||
telebit http /module/path example.com # forward https traffic for example.com to port 3001
|
||||
|
||||
|
||||
################
|
||||
# TCP #
|
||||
################
|
||||
|
||||
telebit tcp <handler> [servername] [opts]
|
||||
|
||||
telebit tcp none # remove all tcp handlers
|
||||
telebit tcp 5050 # forward all tcp to port 5050
|
||||
telebit tcp /module/path # handle all tcp with a node module
|
||||
|
||||
telebit tcp none 6565 # remove tcp handler from external port 6565
|
||||
telebit tcp 5050 6565 # forward external port 6565 to local 5050
|
||||
telebit tcp /module/path 6565 # handle external port 6565 with a node module
|
||||
|
||||
telebit ssh disable # disable ssh access
|
||||
telebit ssh 22 # port-forward all ssh connections to port 22
|
||||
|
||||
telebit save # save http and tcp configuration changes
|
||||
```
|
||||
|
||||
### Using SSH
|
||||
|
||||
SSH over HTTPS
|
||||
```
|
||||
ssh -o ProxyCommand='openssl s_client -connect %h:443 -servername %h -quiet' lucky-duck-42.telebit.cloud
|
||||
```
|
||||
|
||||
SSH over non-standard port
|
||||
```
|
||||
ssh lucky-duck-42.telebit.cloud -p 3031
|
||||
```
|
||||
|
||||
Daemon Usage (non-global)
|
||||
============
|
||||
|
||||
```bash
|
||||
~/Applications/bin/node ~/Applications/bin/telebitd.js --config ~/.config/telebit/telebitd.yml
|
||||
```
|
||||
|
||||
Options
|
||||
|
||||
`~/.config/telebit/telebitd.yml:`
|
||||
```
|
||||
email: 'jon@example.com' # must be valid (for certificate recovery and security alerts)
|
||||
agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
|
||||
relay: wss://telebit.cloud # a Telebit Relay instance
|
||||
community_member: true # receive infrequent relevant but non-critical updates
|
||||
telemetry: true # contribute to project telemetric data
|
||||
secret: '' # Secret with which to sign Tokens for authorization
|
||||
#token: '' # A signed Token for authorization
|
||||
ssh_auto: 22 # forward ssh-looking packets, from any connection, to port 22
|
||||
servernames: # servernames that will be forwarded here
|
||||
example.com: {}
|
||||
```
|
||||
|
||||
Choosing A Relay
|
||||
================
|
||||
|
||||
You can create a free or paid account at <https://telebit.cloud>
|
||||
or you can run [Telebit Relay](https://git.coolaj86.com/coolaj86/telebitd.js)
|
||||
open source on a VPS (Vultr, Digital Ocean)
|
||||
or your Raspberry Pi at home (with port-forwarding).
|
||||
|
||||
Only connect to Telebit Relays that you trust.
|
||||
|
||||
<!--
|
||||
## Important Defaults
|
||||
|
||||
The default behaviors work great for newbies,
|
||||
but can be confusing or annoying to experienced networking veterans.
|
||||
|
||||
See the **Advanced Configuration** section below for more details.
|
||||
|
||||
```
|
||||
redirect:
|
||||
example.com/foo: /bar
|
||||
'*': whatever.com/
|
||||
vhost: # securely serve local sites from this path (or false)
|
||||
example.com: /srv/example.com # (uses template string, i.e. /var/www/:hostname/public)
|
||||
'*': /srv/www/:hostname
|
||||
reverse_proxy: /srv/
|
||||
example.com: 3000
|
||||
'*': 3000
|
||||
terminate_tls:
|
||||
'example.com': 3000
|
||||
'*': 3000
|
||||
tls:
|
||||
'example.com': 8443
|
||||
'*': 8443
|
||||
port_forward:
|
||||
2020: 2020
|
||||
'*': 4040
|
||||
|
||||
greenlock:
|
||||
store: le-store-certbot # certificate storage plugin
|
||||
config_dir: etc/acme # directory for ssl certificates
|
||||
```
|
||||
|
||||
Using Telebit with node.js
|
||||
--------------------------
|
||||
|
||||
Telebit has two parts:
|
||||
* the local server
|
||||
* the relay service
|
||||
|
||||
This repository is for the local server, which you run on the computer or device that you would like to access.
|
||||
|
||||
This is the portion that runs on your computer
|
||||
You will need both Telebit (this, telebit.js) and a Telebit Relay
|
||||
(such as [telebitd.js](https://git.coolaj86.com/coolaj86/telebitd.js)).
|
||||
|
||||
You can **integrate telebit.js into your existing codebase** or use the **standalone CLI**.
|
||||
| Sponsored by [ppl](https://ppl.family) | [Telebit Relay](https://git.coolaj86.com/coolaj86/telebitd.js) | **Telebit** |
|
||||
|
||||
* CLI
|
||||
* Node.js Library
|
||||
* Browser Library
|
||||
* Library
|
||||
|
||||
Telebit CLI
|
||||
-----------
|
||||
CLI
|
||||
===
|
||||
|
||||
Installs Telebit Remote as `telebit`
|
||||
(for those that regularly use `telebit` but still like commandline completion).
|
||||
Installs as `stunnel.js` with the alias `jstunnel`
|
||||
(for those that regularly use `stunnel` but still like commandline completion).
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install -g telebit
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g 'https://git.coolaj86.com/coolaj86/telebit.js.git#v1'
|
||||
npm install -g 'git+https://git@git.coolaj86.com/coolaj86/tunnel-client.js.git#v1'
|
||||
```
|
||||
|
||||
Or if you want to bow down to the kings of the centralized dictator-net:
|
||||
|
||||
How to use Telebit Remote with your own instance of Telebit Relay:
|
||||
```bash
|
||||
npm install -g stunnel
|
||||
```
|
||||
|
||||
### Usage with OAuth3.org
|
||||
|
||||
The OAuth3.org tunnel service is in Beta.
|
||||
|
||||
**Terms of Service**: The Software and Services shall be used for Good, not Evil.
|
||||
Examples of good: education, business, pleasure. Examples of evil: crime, abuse, extortion.
|
||||
|
||||
```bash
|
||||
telebitd \
|
||||
stunnel.js --agree-tos --email john@example.com --locals http:*:4080,https:*:8443 --device
|
||||
```
|
||||
|
||||
```bash
|
||||
stunnel.js \
|
||||
--agree-tos --email <EMAIL> \
|
||||
--locals <List of <SCHEME>:<EXTERNAL_DOMAINNAME>:<INTERNAL_PORT>> \
|
||||
--device [HOSTNAME] \
|
||||
--domains [Comma-separated list of domains to attach to device] \
|
||||
--oauth3-url <Tunnel Service OAuth3 URL>
|
||||
```
|
||||
|
||||
### Advanced Usage (DIY)
|
||||
|
||||
How to use `stunnel.js` with your own instance of `stunneld.js`:
|
||||
|
||||
```bash
|
||||
stunnel.js \
|
||||
--locals <<external domain name>> \
|
||||
--relay wss://<<tunnel domain>>:<<tunnel port>> \
|
||||
--stunneld wss://<<tunnel domain>>:<<tunnel port>> \
|
||||
--secret <<128-bit hex key>>
|
||||
```
|
||||
|
||||
```bash
|
||||
telebitd --locals john.example.com --relay wss://tunnel.example.com:443 --secret abc123
|
||||
stunnel.js --locals john.example.com --stunneld wss://tunnel.example.com:443 --secret abc123
|
||||
```
|
||||
|
||||
```bash
|
||||
telebitd \
|
||||
stunnel.js \
|
||||
--locals <<protocol>>:<<external domain name>>:<<local port>> \
|
||||
--relay wss://<<tunnel domain>>:<<tunnel port>> \
|
||||
--stunneld wss://<<tunnel domain>>:<<tunnel port>> \
|
||||
--secret <<128-bit hex key>>
|
||||
```
|
||||
|
||||
```bash
|
||||
telebitd \
|
||||
stunnel.js \
|
||||
--locals http:john.example.com:3000,https:john.example.com \
|
||||
--relay wss://tunnel.example.com:443 \
|
||||
--stunneld wss://tunnel.example.com:443 \
|
||||
--secret abc123
|
||||
```
|
||||
|
||||
```
|
||||
--secret the same secret used by the Telebit Relay (for authentication)
|
||||
--secret the same secret used by stunneld (used for authentication)
|
||||
--locals comma separated list of <proto>:<servername>:<port> to which
|
||||
incoming http and https should be forwarded
|
||||
--relay the domain or ip address at which you are running Telebit Relay
|
||||
-k, --insecure ignore invalid ssl certificates from relay
|
||||
--stunneld the domain or ip address at which you are running stunneld.js
|
||||
-k, --insecure ignore invalid ssl certificates from stunneld
|
||||
```
|
||||
|
||||
Node.js Library
|
||||
Library
|
||||
=======
|
||||
|
||||
### Example
|
||||
|
||||
```javascript
|
||||
var Telebit = require('telebit');
|
||||
var stunnel = require('stunnel');
|
||||
|
||||
Telebit.connect({
|
||||
relay: 'wss://tunnel.example.com'
|
||||
stunnel.connect({
|
||||
stunneld: 'wss://tunnel.example.com'
|
||||
, token: '...'
|
||||
, locals: [
|
||||
// defaults to sending http to local port 80 and https to local port 443
|
||||
|
@ -388,7 +132,7 @@ local handler and the tunnel handler.
|
|||
You could do a little magic like this:
|
||||
|
||||
```js
|
||||
Telebit.connect({
|
||||
stunnel.connect({
|
||||
// ...
|
||||
, net: {
|
||||
createConnection: function (info, cb) {
|
||||
|
@ -422,108 +166,3 @@ Telebit.connect({
|
|||
}
|
||||
});
|
||||
```
|
||||
|
||||
Advanced Configuration
|
||||
======================
|
||||
|
||||
There is no configuration for these yet,
|
||||
but we believe it is important to add them.
|
||||
|
||||
### http to https
|
||||
|
||||
By default http connections are redirected to https.
|
||||
|
||||
If for some reason you need raw access to unencrypted http
|
||||
you'll need to set it manually.
|
||||
|
||||
Proposed configuration:
|
||||
|
||||
```
|
||||
insecure_http:
|
||||
proxy: true # add X-Forward-* headers
|
||||
port: 3000 # connect to port 3000
|
||||
hostnames: # only these hostnames will be left insecure
|
||||
- example.com
|
||||
```
|
||||
|
||||
**Note**: In the future unencrypted connections will only be allowed
|
||||
on self-hosted and paid-hosted Telebit Relays. We don't want the
|
||||
legal liability of transmitting your data in the clear, thanks. :p
|
||||
|
||||
### TLS Termination (Secure SSL decryption)
|
||||
|
||||
Telebit is designed for end-to-end security.
|
||||
|
||||
For convenience the Telebit Remote client uses Greenlock to handle all
|
||||
HTTPS connections and then connect to a local webserver with the correct proxy headers.
|
||||
|
||||
However, if you want to handle the encrypted connection directly, you can:
|
||||
|
||||
Proposed Configuration:
|
||||
|
||||
```
|
||||
tls:
|
||||
example.com: 3000 # specific servername
|
||||
'*': 3000 # all servernames
|
||||
'!': 3000 # missing servername
|
||||
```
|
||||
|
||||
TODO
|
||||
====
|
||||
|
||||
Install for user
|
||||
* https://wiki.archlinux.org/index.php/Systemd/User
|
||||
* https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html
|
||||
* `sudo launchctl load -w ~/Library/LaunchAgents/cloud.telebit.remote`
|
||||
* https://serverfault.com/questions/194832/how-to-start-stop-restart-launchd-services-from-the-command-line
|
||||
-->
|
||||
|
||||
Check Logs
|
||||
==========
|
||||
|
||||
**Linux**:
|
||||
|
||||
```
|
||||
SYSTEMD_LOG_LEVEL=debug journalctl -xef --user-unit=telebit
|
||||
```
|
||||
|
||||
**macOS**:
|
||||
|
||||
```
|
||||
tail -f ~/local/share/telebit/var/log/info.log
|
||||
```
|
||||
|
||||
```
|
||||
tail -f ~/.local/share/telebit/var/log/error.log
|
||||
```
|
||||
|
||||
Uninstall
|
||||
=======
|
||||
|
||||
**Linux**:
|
||||
|
||||
```
|
||||
systemctl --user disable telebit; systemctl --user stop telebit
|
||||
rm -f ~/.config/systemd/user/telebit.service
|
||||
rm -rf ~/telebit ~/Applications/telebit
|
||||
rm -rf ~/.config/telebit ~/.local/share/telebit
|
||||
```
|
||||
|
||||
**macOS**:
|
||||
|
||||
```
|
||||
launchctl unload -w ~/Library/LaunchAgents/cloud.telebit.remote.plist
|
||||
rm -f ~/Library/LaunchAgents/cloud.telebit.remote.plist
|
||||
rm -rf ~/telebit ~/Applications/telebit
|
||||
rm -rf ~/.config/telebit ~/.local/share/telebit
|
||||
```
|
||||
|
||||
Browser Library
|
||||
=======
|
||||
|
||||
This is implemented with websockets, so you should be able to
|
||||
|
||||
LICENSE
|
||||
=======
|
||||
|
||||
Copyright 2016-2018+ AJ ONeal
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
TODO
|
||||
|
||||
* [*] Work with Secure WebSockets
|
||||
* [ ] Hijack HTTPS connection directly (without WebSockets)
|
||||
* [p] Raw TCP (for transporting https once, not twice) (partial)
|
||||
* [ ] Let's Encrypt Support (for connecting to a plain http server locally)
|
|
@ -0,0 +1,292 @@
|
|||
#!/usr/bin/env node
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var pkg = require('../package.json');
|
||||
|
||||
var program = require('commander');
|
||||
var url = require('url');
|
||||
var stunnel = require('../wsclient.js');
|
||||
|
||||
var domainsMap = {};
|
||||
var services = {};
|
||||
|
||||
function collectDomains(val, memo) {
|
||||
var vals = val.split(/,/g);
|
||||
|
||||
function parseProxy(location) {
|
||||
// john.example.com
|
||||
// http:john.example.com:3000
|
||||
// http://john.example.com:3000
|
||||
var parts = location.split(':');
|
||||
if (1 === parts.length) {
|
||||
// john.example.com -> :john.example.com:0
|
||||
parts[1] = parts[0];
|
||||
|
||||
parts[0] = '';
|
||||
parts[2] = 0;
|
||||
}
|
||||
else if (2 === parts.length) {
|
||||
throw new Error("invalid arguments for --domains, should use the format <domainname> or <scheme>:<domainname>:<local-port>");
|
||||
}
|
||||
if (!parts[1]) {
|
||||
throw new Error("invalid arguments for --domains, should use the format <domainname> or <scheme>:<domainname>:<local-port>");
|
||||
}
|
||||
|
||||
parts[0] = parts[0].toLowerCase();
|
||||
parts[1] = parts[1].toLowerCase().replace(/(\/\/)?/, '');
|
||||
parts[2] = parseInt(parts[2], 10) || 0;
|
||||
|
||||
memo.push({
|
||||
protocol: parts[0]
|
||||
, hostname: parts[1]
|
||||
, port: parts[2]
|
||||
});
|
||||
}
|
||||
|
||||
vals.map(function (val) {
|
||||
return parseProxy(val);
|
||||
});
|
||||
|
||||
return memo;
|
||||
}
|
||||
function collectProxies(val, memo) {
|
||||
var vals = val.split(/,/g);
|
||||
|
||||
function parseProxy(location) {
|
||||
// john.example.com
|
||||
// https:3443
|
||||
// http:john.example.com:3000
|
||||
// http://john.example.com:3000
|
||||
var parts = location.split(':');
|
||||
var dual = false;
|
||||
if (1 === parts.length) {
|
||||
// john.example.com -> :john.example.com:0
|
||||
parts[1] = parts[0];
|
||||
|
||||
parts[0] = '';
|
||||
parts[2] = 0;
|
||||
|
||||
dual = true;
|
||||
}
|
||||
else if (2 === parts.length) {
|
||||
// https:3443 -> https:*:3443
|
||||
parts[2] = parts[1];
|
||||
|
||||
parts[1] = '*';
|
||||
}
|
||||
|
||||
parts[0] = parts[0].toLowerCase();
|
||||
parts[1] = parts[1].toLowerCase().replace(/(\/\/)?/, '') || '*';
|
||||
parts[2] = parseInt(parts[2], 10) || 0;
|
||||
if (!parts[2]) {
|
||||
// TODO grab OS list of standard ports?
|
||||
if (!parts[0] || 'http' === parts[0]) {
|
||||
parts[2] = 80;
|
||||
}
|
||||
else if ('https' === parts[0]) {
|
||||
parts[2] = 443;
|
||||
}
|
||||
else {
|
||||
throw new Error("port must be specified - ex: tls:*:1337");
|
||||
}
|
||||
}
|
||||
|
||||
memo.push({
|
||||
protocol: parts[0] || 'https'
|
||||
, hostname: parts[1]
|
||||
, port: parts[2] || 443
|
||||
});
|
||||
|
||||
if (dual) {
|
||||
memo.push({
|
||||
protocol: 'http'
|
||||
, hostname: parts[1]
|
||||
, port: 80
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vals.map(function (val) {
|
||||
return parseProxy(val);
|
||||
});
|
||||
|
||||
return memo;
|
||||
}
|
||||
|
||||
program
|
||||
.version(pkg.version)
|
||||
//.command('jsurl <url>')
|
||||
.arguments('<url>')
|
||||
.action(function (url) {
|
||||
program.url = url;
|
||||
})
|
||||
.option('-k --insecure', 'Allow TLS connections to stunneld without valid certs (rejectUnauthorized: false)')
|
||||
.option('--locals <LIST>', 'comma separated list of <proto>:<port> to which matching incoming http and https should forward (reverse proxy). Ex: https:8443,smtps:8465', collectProxies, [ ]) // --reverse-proxies
|
||||
.option('--domains <LIST>', 'comma separated list of domain names to set to the tunnel (to capture a specific protocol to a specific local port use the format https:example.com:1337 instead). Ex: example.com,example.net', collectDomains, [ ])
|
||||
.option('--device [HOSTNAME]', 'Tunnel all domains associated with this device instead of specific domainnames. Use with --locals <proto>:<port>. Ex: macbook-pro.local (the output of `hostname`)')
|
||||
.option('--stunneld <URL>', 'the domain (or ip address) at which you are running stunneld.js (the proxy)') // --proxy
|
||||
.option('--secret <STRING>', 'the same secret used by stunneld (used for JWT authentication)')
|
||||
.option('--token <STRING>', 'a pre-generated token for use with stunneld (instead of generating one with --secret)')
|
||||
.option('--agree-tos', 'agree to the Daplie Terms of Service (requires user validation)')
|
||||
.option('--email <EMAIL>', 'email address (or cloud address) for user validation')
|
||||
.option('--oauth3-url <URL>', 'Cloud Authentication to use (default: https://oauth3.org)')
|
||||
.parse(process.argv)
|
||||
;
|
||||
|
||||
function connectTunnel() {
|
||||
program.net = {
|
||||
createConnection: function (info, cb) {
|
||||
// data is the hello packet / first chunk
|
||||
// info = { data, servername, port, host, remoteFamily, remoteAddress, remotePort }
|
||||
var net = require('net');
|
||||
// socket = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
|
||||
var socket = net.createConnection({ port: info.port, host: info.host }, cb);
|
||||
return socket;
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(program.services).forEach(function (protocol) {
|
||||
var subServices = program.services[protocol];
|
||||
Object.keys(subServices).forEach(function (hostname) {
|
||||
console.info('[local proxy]', protocol + '://' + hostname + ' => ' + subServices[hostname]);
|
||||
});
|
||||
});
|
||||
console.info('');
|
||||
|
||||
var tun = stunnel.connect({
|
||||
stunneld: program.stunneld
|
||||
, locals: program.locals
|
||||
, services: program.services
|
||||
, net: program.net
|
||||
, insecure: program.insecure
|
||||
, token: program.token
|
||||
});
|
||||
|
||||
function sigHandler() {
|
||||
console.log('SIGINT');
|
||||
|
||||
// We want to handle cleanup properly unless something is broken in our cleanup process
|
||||
// that prevents us from exitting, in which case we want the user to be able to send
|
||||
// the signal again and exit the way it normally would.
|
||||
process.removeListener('SIGINT', sigHandler);
|
||||
tun.end();
|
||||
}
|
||||
process.on('SIGINT', sigHandler);
|
||||
}
|
||||
|
||||
function rawTunnel() {
|
||||
program.stunneld = program.stunneld || 'wss://tunnel.daplie.com';
|
||||
|
||||
if (!(program.secret || program.token)) {
|
||||
console.error("You must use --secret or --token with --stunneld");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
var location = url.parse(program.stunneld);
|
||||
if (!location.protocol || /\./.test(location.protocol)) {
|
||||
program.stunneld = 'wss://' + program.stunneld;
|
||||
location = url.parse(program.stunneld);
|
||||
}
|
||||
var aud = location.hostname + (location.port ? ':' + location.port : '');
|
||||
program.stunneld = location.protocol + '//' + aud;
|
||||
|
||||
if (!program.token) {
|
||||
var jwt = require('jsonwebtoken');
|
||||
var tokenData = {
|
||||
domains: Object.keys(domainsMap).filter(Boolean)
|
||||
, aud: aud
|
||||
};
|
||||
|
||||
program.token = jwt.sign(tokenData, program.secret);
|
||||
}
|
||||
|
||||
connectTunnel();
|
||||
}
|
||||
|
||||
function daplieTunnel() {
|
||||
//var OAUTH3 = require('oauth3.js');
|
||||
var Oauth3Cli = require('oauth3.js/bin/oauth3.js');
|
||||
require('oauth3.js/oauth3.tunnel.js');
|
||||
return Oauth3Cli.login({
|
||||
email: program.email
|
||||
, providerUri: program.oauth3Url || 'oauth3.org'
|
||||
}).then(function (oauth3) {
|
||||
var data = { device: null, domains: [] };
|
||||
var domains = Object.keys(domainsMap).filter(Boolean);
|
||||
if (program.device) {
|
||||
// TODO use device API to select device by id
|
||||
data.device = { hostname: program.device };
|
||||
if (true === program.device) {
|
||||
data.device.hostname = require('os').hostname();
|
||||
console.log("Using device hostname '" + data.device.hostname + "'");
|
||||
}
|
||||
}
|
||||
if (domains.length) {
|
||||
data.domains = domains;
|
||||
}
|
||||
return oauth3.api('tunnel.token', { data: data }).then(function (results) {
|
||||
var token = new Buffer(results.jwt.split('.')[1], 'base64').toString('utf8');
|
||||
console.info('');
|
||||
console.info('tunnel token issued:');
|
||||
console.info(token);
|
||||
console.info('');
|
||||
program.token = results.jwt;
|
||||
program.stunneld = results.tunnelUrl || ('wss://' + token.aud + '/');
|
||||
|
||||
connectTunnel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
program.locals = (program.locals || []).concat(program.domains || []);
|
||||
program.locals.forEach(function (proxy) {
|
||||
// Create a map from which we can derive a list of all domains we want forwarded to us.
|
||||
if (proxy.hostname && proxy.hostname !== '*') {
|
||||
domainsMap[proxy.hostname] = true;
|
||||
}
|
||||
|
||||
// Create a map of which port different protocols should be forwarded to, allowing for specific
|
||||
// domains to go to different ports if need be (though that only works for HTTP and HTTPS).
|
||||
if (proxy.protocol && proxy.port) {
|
||||
services[proxy.protocol] = services[proxy.protocol] || {};
|
||||
|
||||
if (/http/.test(proxy.protocol) && proxy.hostname && proxy.hostname !== '*') {
|
||||
services[proxy.protocol][proxy.hostname] = proxy.port;
|
||||
}
|
||||
else {
|
||||
if (services[proxy.protocol]['*'] && services[proxy.protocol]['*'] !== proxy.port) {
|
||||
console.error('cannot forward generic', proxy.protocol, 'traffic to multiple ports');
|
||||
process.exit(1);
|
||||
}
|
||||
else {
|
||||
services[proxy.protocol]['*'] = proxy.port;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(domainsMap).length === 0) {
|
||||
console.error('no domains specified');
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we have generic ports for HTTP and HTTPS
|
||||
services.https = services.https || {};
|
||||
services.https['*'] = services.https['*'] || 8443;
|
||||
|
||||
services.http = services.http || {};
|
||||
services.http['*'] = services.http['*'] || services.https['*'];
|
||||
|
||||
program.services = services;
|
||||
|
||||
if (!(program.secret || program.token) && !program.stunneld) {
|
||||
daplieTunnel();
|
||||
}
|
||||
else {
|
||||
rawTunnel();
|
||||
}
|
||||
|
||||
}());
|
832
bin/telebit.js
832
bin/telebit.js
|
@ -1,832 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var pkg = require('../package.json');
|
||||
var os = require('os');
|
||||
|
||||
//var url = require('url');
|
||||
var path = require('path');
|
||||
var http = require('http');
|
||||
//var https = require('https');
|
||||
var YAML = require('js-yaml');
|
||||
var recase = require('recase').create({});
|
||||
var camelCopy = recase.camelCopy.bind(recase);
|
||||
//var snakeCopy = recase.snakeCopy.bind(recase);
|
||||
|
||||
var urequest = require('@coolaj86/urequest');
|
||||
var common = require('../lib/cli-common.js');
|
||||
|
||||
var argv = process.argv.slice(2);
|
||||
|
||||
var argIndex = argv.indexOf('--config');
|
||||
var confpath;
|
||||
var useTty;
|
||||
var state = {};
|
||||
if (-1 === argIndex) {
|
||||
argIndex = argv.indexOf('-c');
|
||||
}
|
||||
if (-1 !== argIndex) {
|
||||
confpath = argv.splice(argIndex, 2)[1];
|
||||
}
|
||||
argIndex = argv.indexOf('--tty');
|
||||
if (-1 !== argIndex) {
|
||||
useTty = argv.splice(argIndex, 1);
|
||||
}
|
||||
|
||||
function help() {
|
||||
//console.info('');
|
||||
//console.info('Telebit Remote v' + pkg.version);
|
||||
console.info('');
|
||||
console.info('Usage:');
|
||||
console.info('');
|
||||
console.info('\ttelebit [--config <path>] <module> <module-options>');
|
||||
console.info('');
|
||||
console.info('Examples:');
|
||||
console.info('');
|
||||
//console.info('\ttelebit init # bootstrap the config files');
|
||||
//console.info('');
|
||||
console.info('\ttelebit status # whether enabled or disabled');
|
||||
console.info('\ttelebit enable # disallow incoming connections');
|
||||
console.info('\ttelebit disable # allow incoming connections');
|
||||
console.info('');
|
||||
console.info('\ttelebit list # list rules for servernames and ports');
|
||||
console.info('');
|
||||
console.info('\ttelebit http none # remove all https handlers');
|
||||
console.info('\ttelebit http 3000 # forward all https traffic to port 3000');
|
||||
console.info('\ttelebit http /module/path # load a node module to handle all https traffic');
|
||||
console.info('');
|
||||
console.info('\ttelebit http none example.com # remove https handler from example.com');
|
||||
console.info('\ttelebit http 3001 example.com # forward https traffic for example.com to port 3001');
|
||||
console.info('\ttelebit http /module/path example.com # forward https traffic for example.com to port 3001');
|
||||
console.info('');
|
||||
console.info('\ttelebit tcp none # remove all tcp handlers');
|
||||
console.info('\ttelebit tcp 5050 # forward all tcp to port 5050');
|
||||
console.info('\ttelebit tcp /module/path # handle all tcp with a node module');
|
||||
console.info('');
|
||||
console.info('\ttelebit tcp none 6565 # remove tcp handler from external port 6565');
|
||||
console.info('\ttelebit tcp 5050 6565 # forward external port 6565 to local 5050');
|
||||
console.info('\ttelebit tcp /module/path 6565 # handle external port 6565 with a node module');
|
||||
console.info('');
|
||||
console.info('Config:');
|
||||
console.info('');
|
||||
console.info('\tSee https://git.coolaj86.com/coolaj86/telebit.js');
|
||||
console.info('');
|
||||
console.info('');
|
||||
}
|
||||
|
||||
var verstr = [ pkg.name + ' remote v' + pkg.version ];
|
||||
if (!confpath) {
|
||||
confpath = path.join(os.homedir(), '.config/telebit/telebit.yml');
|
||||
verstr.push('(--config "' + confpath + '")');
|
||||
}
|
||||
|
||||
if (-1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) {
|
||||
help();
|
||||
process.exit(0);
|
||||
}
|
||||
if (!confpath || /^--/.test(confpath)) {
|
||||
help();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function askForConfig(state, mainCb) {
|
||||
var fs = require('fs');
|
||||
var ttyname = '/dev/tty';
|
||||
var stdin = useTty ? fs.createReadStream(ttyname, {
|
||||
fd: fs.openSync(ttyname, fs.constants.O_RDONLY | fs.constants.O_NOCTTY)
|
||||
}) : process.stdin;
|
||||
var readline = require('readline');
|
||||
var rl = readline.createInterface({
|
||||
input: stdin
|
||||
, output: process.stdout
|
||||
// https://github.com/nodejs/node/issues/21771
|
||||
// https://github.com/nodejs/node/issues/21319
|
||||
, terminal: !/^win/i.test(os.platform()) && !useTty
|
||||
});
|
||||
state._useTty = useTty;
|
||||
|
||||
// NOTE: Use of setTimeout
|
||||
// We're using setTimeout just to make the user experience a little
|
||||
// nicer, as if we're doing something inbetween steps, so that it
|
||||
// is a smooth rather than jerky experience.
|
||||
// >= 300ms is long enough to become distracted and change focus (a full blink, time for an idea to form as a thought)
|
||||
// <= 100ms is shorter than normal human reaction time (ability to place events chronologically, which happened first)
|
||||
// ~ 150-250ms is the sweet spot for most humans (long enough to notice change and not be jarred, but stay on task)
|
||||
var firstSet = [
|
||||
function askEmail(cb) {
|
||||
if (state.config.email) { cb(); return; }
|
||||
//console.info("");
|
||||
console.info("Welcome!");
|
||||
console.info("");
|
||||
console.info("By using Telebit you agree to:");
|
||||
console.info("");
|
||||
console.info(" [x] Accept the Telebit™ terms of service");
|
||||
console.info(" [x] Accept the Let's Encrypt™ terms of service");
|
||||
console.info("");
|
||||
console.info("Enter your email to agree and login/create your account:");
|
||||
console.info("");
|
||||
// TODO attempt to read email from npmrc or the like?
|
||||
rl.question('email: ', function (email) {
|
||||
email = /@/.test(email) && email.trim();
|
||||
if (!email) { askEmail(cb); return; }
|
||||
state.config.email = email.trim();
|
||||
state.config.agreeTos = true;
|
||||
console.info("");
|
||||
setTimeout(cb, 250);
|
||||
});
|
||||
}
|
||||
, function askRelay(cb) {
|
||||
function checkRelay(relay) {
|
||||
// TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json
|
||||
if (!relay) { relay = 'telebit.cloud'; }
|
||||
relay = relay.trim();
|
||||
var urlstr = common.parseUrl(relay) + common.apiDirectory;
|
||||
urequest({ url: urlstr, json: true }, function (err, resp, body) {
|
||||
if (err) {
|
||||
console.error("[Network Error] Failed to retrieve '" + urlstr + "'");
|
||||
console.error(err);
|
||||
askRelay(cb);
|
||||
return;
|
||||
}
|
||||
if (200 !== resp.statusCode || (Buffer.isBuffer(body) || 'object' !== typeof body) || !body.api_host) {
|
||||
console.warn("===================");
|
||||
console.warn(" WARNING ");
|
||||
console.warn("===================");
|
||||
console.warn("");
|
||||
console.warn("[" + resp.statusCode + "] '" + urlstr + "'");
|
||||
console.warn("This server does not describe a current telebit version (but it may still work).");
|
||||
console.warn("");
|
||||
console.warn(body);
|
||||
} else if (body && body.pair_request) {
|
||||
state._can_pair = true;
|
||||
}
|
||||
state.config.relay = relay;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
if (state.config.relay) { checkRelay(); return; }
|
||||
console.info("");
|
||||
console.info("");
|
||||
console.info("What relay will you be using? (press enter for default)");
|
||||
console.info("");
|
||||
rl.question('relay [default: telebit.cloud]: ', checkRelay);
|
||||
}
|
||||
, function checkRelay(cb) {
|
||||
nextSet = [];
|
||||
if ('telebit.cloud' !== state.config.relay) {
|
||||
nextSet = nextSet.concat(standardSet);
|
||||
}
|
||||
if (!state._can_pair) {
|
||||
nextSet = nextSet.concat(fossSet);
|
||||
}
|
||||
cb();
|
||||
}
|
||||
];
|
||||
var standardSet = [
|
||||
// There are questions that we need to aks in the CLI
|
||||
// if we can't guarantee that they are being asked in the web interface
|
||||
function askAgree(cb) {
|
||||
if (state.config.agreeTos) { cb(); return; }
|
||||
console.info("");
|
||||
console.info("");
|
||||
console.info("Do you accept the terms of service for each and all of the following?");
|
||||
console.info("");
|
||||
console.info("\tTelebit - End-to-End Encrypted Relay");
|
||||
console.info("\tGreenlock - Automated HTTPS");
|
||||
console.info("\tLet's Encrypt - TLS Certificates");
|
||||
console.info("");
|
||||
console.info("Type 'y' or 'yes' to accept these Terms of Service.");
|
||||
console.info("");
|
||||
rl.question('agree to all? [y/N]: ', function (resp) {
|
||||
resp = resp.trim();
|
||||
if (!/^y(es)?$/i.test(resp) && 'true' !== resp) {
|
||||
throw new Error("You didn't accept the Terms of Service... not sure what to do...");
|
||||
}
|
||||
state.config.agreeTos = true;
|
||||
console.info("");
|
||||
setTimeout(cb, 250);
|
||||
});
|
||||
}
|
||||
, function askUpdates(cb) {
|
||||
// required means transactional, security alerts, mandatory updates
|
||||
var options = [ 'newsletter', 'important', 'required' ];
|
||||
if (-1 !== options.indexOf(state._updates)) { cb(); return; }
|
||||
console.info("");
|
||||
console.info("");
|
||||
console.info("What updates would you like to receive? (" + options.join(',') + ")");
|
||||
console.info("");
|
||||
rl.question('messages (default: important): ', function (updates) {
|
||||
state._updates = (updates || '').trim().toLowerCase();
|
||||
if (!state._updates) { state._updates = 'important'; }
|
||||
if (-1 === options.indexOf(state._updates)) { askUpdates(cb); return; }
|
||||
|
||||
if ('newsletter' === state._updates) {
|
||||
state.config.newsletter = true;
|
||||
state.config.communityMember = true;
|
||||
} else if ('important' === state._updates) {
|
||||
state.config.communityMember = true;
|
||||
}
|
||||
|
||||
setTimeout(cb, 250);
|
||||
});
|
||||
}
|
||||
, function askTelemetry(cb) {
|
||||
if (state.config.telemetry) { cb(); return; }
|
||||
console.info("");
|
||||
console.info("");
|
||||
console.info("Contribute project telemetry data? (press enter for default [yes])");
|
||||
console.info("");
|
||||
rl.question('telemetry [Y/n]: ', function (telemetry) {
|
||||
if (!telemetry || /^y(es)?$/i.test(telemetry)) {
|
||||
state.config.telemetry = true;
|
||||
}
|
||||
setTimeout(cb, 250);
|
||||
});
|
||||
}
|
||||
];
|
||||
var fossSet = [
|
||||
function askTokenOrSecret(cb) {
|
||||
if (state._can_pair || state.token || state.config.token
|
||||
|| state.secret || state.config.secret) { cb(); return; }
|
||||
console.info("");
|
||||
console.info("");
|
||||
console.info("What's your authorization for '" + state.config.relay + "'?");
|
||||
console.info("");
|
||||
// TODO check .well-known to learn supported token types
|
||||
console.info("Currently supported:");
|
||||
console.info("");
|
||||
console.info("\tToken (JWT format)");
|
||||
console.info("\tShared Secret (HMAC hex)");
|
||||
//console.info("\tPrivate key (hex)");
|
||||
console.info("");
|
||||
rl.question('auth: ', function (resp) {
|
||||
var jwt = require('jsonwebtoken');
|
||||
resp = (resp || '').trim();
|
||||
try {
|
||||
jwt.decode(resp);
|
||||
state.config.token = resp;
|
||||
} catch(e) {
|
||||
// is not jwt
|
||||
}
|
||||
if (!state.config.token) {
|
||||
resp = resp.toLowerCase();
|
||||
if (resp === Buffer.from(resp, 'hex').toString('hex')) {
|
||||
state.config.secret = resp;
|
||||
}
|
||||
}
|
||||
if (!state.config.token && !state.config.secret) {
|
||||
askTokenOrSecret(cb);
|
||||
return;
|
||||
}
|
||||
setTimeout(cb, 250);
|
||||
});
|
||||
}
|
||||
, function askServernames(cb) {
|
||||
if (!state.config.secret || state.config._servernames) { cb(); return; }
|
||||
console.info("");
|
||||
console.info("");
|
||||
console.info("What servername(s) will you be relaying here?");
|
||||
console.info("(use a comma-separated list such as example.com,example.net)");
|
||||
console.info("");
|
||||
rl.question('domain(s): ', function (resp) {
|
||||
resp = (resp || '').trim().split(/,/g);
|
||||
if (!resp.length) { askServernames(); return; }
|
||||
// TODO validate the domains
|
||||
state.config._servernames = resp;
|
||||
setTimeout(cb, 250);
|
||||
});
|
||||
}
|
||||
, function askPorts(cb) {
|
||||
if (!state.config.secret || state.config._ports) { cb(); return; }
|
||||
console.info("");
|
||||
console.info("");
|
||||
console.info("What tcp port(s) will you be relaying here?");
|
||||
console.info("(use a comma-separated list such as 2222,5050)");
|
||||
console.info("");
|
||||
rl.question('port(s) [default:none]: ', function (resp) {
|
||||
resp = (resp || '').trim().split(/,/g);
|
||||
if (!resp.length) { askPorts(); return; }
|
||||
// TODO validate the domains
|
||||
state.config._ports = resp;
|
||||
setTimeout(cb, 250);
|
||||
});
|
||||
}
|
||||
];
|
||||
var nextSet = firstSet;
|
||||
|
||||
function next() {
|
||||
var q = nextSet.shift();
|
||||
if (!q) {
|
||||
// https://github.com/nodejs/node/issues/21319
|
||||
if (useTty) { try { stdin.push(null); } catch(e) { /*ignore*/ } }
|
||||
rl.close();
|
||||
if (useTty) { try { stdin.close(); } catch(e) { /*ignore*/ } }
|
||||
mainCb(null, state);
|
||||
return;
|
||||
}
|
||||
q(next);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
var utils = {
|
||||
request: function request(opts, fn) {
|
||||
if (!opts) { opts = {}; }
|
||||
var service = opts.service || 'config';
|
||||
var req = http.request({
|
||||
socketPath: state._ipc.path
|
||||
, method: opts.method || 'GET'
|
||||
, path: '/rpc/' + service
|
||||
}, function (resp) {
|
||||
var body = '';
|
||||
|
||||
function finish() {
|
||||
if (200 !== resp.statusCode) {
|
||||
console.warn(resp.statusCode);
|
||||
console.warn(body || ('get' + service + ' failed'));
|
||||
//cb(new Error("not okay"), body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body) { fn(null, null); return; }
|
||||
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch(e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
fn(null, body);
|
||||
}
|
||||
|
||||
if (resp.headers['content-length']) {
|
||||
resp.on('data', function (chunk) {
|
||||
body += chunk.toString();
|
||||
});
|
||||
resp.on('end', function () {
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
req.on('error', function (err) {
|
||||
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
|
||||
// ECONNREFUSED - leftover socket just needs to be restarted
|
||||
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
|
||||
if (opts._taketwo) {
|
||||
console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to.");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
|
||||
if (err) { fn(err); return; }
|
||||
opts._taketwo = true;
|
||||
utils.request(opts, fn);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ('ENOTSOCK' === err.code) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
req.end();
|
||||
}
|
||||
, putConfig: function putConfig(service, args, fn) {
|
||||
var req = http.request({
|
||||
socketPath: state._ipc.path
|
||||
, method: 'POST'
|
||||
, path: '/rpc/' + service + '?_body=' + encodeURIComponent(JSON.stringify(args))
|
||||
}, function (resp) {
|
||||
|
||||
function finish() {
|
||||
if ('function' === typeof fn) {
|
||||
fn(null, resp);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("");
|
||||
if (200 !== resp.statusCode) {
|
||||
console.warn("'" + service + "' may have failed."
|
||||
+ " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log");
|
||||
console.warn(resp.statusCode, body);
|
||||
//cb(new Error("not okay"), body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
console.info("👌");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch(e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if ("AWAIT_AUTH" === body.code) {
|
||||
console.info(body.message);
|
||||
} else if ("CONFIG" === body.code) {
|
||||
delete body.code;
|
||||
console.info(YAML.safeDump(body));
|
||||
} else {
|
||||
if ('http' === body.module) {
|
||||
// TODO we'll support slingshot-ing in the future
|
||||
if (String(body.local) === String(parseInt(body.local, 10))) {
|
||||
console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local);
|
||||
} else {
|
||||
console.info('> Serving ' + body.local + ' as https://' + body.remote);
|
||||
}
|
||||
} else if ('tcp' === body.module) {
|
||||
console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local);
|
||||
} else if ('ssh' === body.module) {
|
||||
console.info('> Forwarding ' + state.config.relay + ' -p ' + body.remote + ' => localhost:' + body.local);
|
||||
console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local);
|
||||
} else {
|
||||
console.info(JSON.stringify(body, null, 2));
|
||||
}
|
||||
console.info();
|
||||
}
|
||||
}
|
||||
|
||||
var body = '';
|
||||
if (resp.headers['content-length']) {
|
||||
resp.on('data', function (chunk) {
|
||||
body += chunk.toString();
|
||||
});
|
||||
resp.on('end', function () {
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
req.on('error', function (err) {
|
||||
console.error('Put Config Error:');
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
req.end();
|
||||
}
|
||||
};
|
||||
|
||||
// Two styles:
|
||||
// http 3000
|
||||
// http modulename
|
||||
function makeRpc(key) {
|
||||
if (key !== argv[0]) {
|
||||
return false;
|
||||
}
|
||||
utils.putConfig(argv[0], argv.slice(1));
|
||||
return true;
|
||||
}
|
||||
|
||||
function packConfig(config) {
|
||||
return Object.keys(config).map(function (key) {
|
||||
var val = config[key];
|
||||
if ('undefined' === val) {
|
||||
throw new Error("'undefined' used as a string value");
|
||||
}
|
||||
if ('undefined' === typeof val) {
|
||||
//console.warn('[DEBUG]', key, 'is present but undefined');
|
||||
return;
|
||||
}
|
||||
if (val && 'object' === typeof val && !Array.isArray(val)) {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
return key + ':' + val; // converts arrays to strings with ,
|
||||
});
|
||||
}
|
||||
|
||||
function getToken(err, state) {
|
||||
if (err) {
|
||||
console.error("Error while initializing config [init]:");
|
||||
throw err;
|
||||
}
|
||||
state.relay = state.config.relay;
|
||||
|
||||
// { _otp, config: {} }
|
||||
common.api.token(state, {
|
||||
error: function (err/*, next*/) {
|
||||
console.error("[Error] common.api.token:");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
, directory: function (dir, next) {
|
||||
//console.log('[directory] Telebit Relay Discovered:');
|
||||
//console.log(dir);
|
||||
state._apiDirectory = dir;
|
||||
next();
|
||||
}
|
||||
, tunnelUrl: function (tunnelUrl, next) {
|
||||
//console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
|
||||
state.wss = tunnelUrl;
|
||||
next();
|
||||
}
|
||||
, requested: function (authReq, next) {
|
||||
//console.log("[requested] Pairing Requested");
|
||||
state.config._otp = state.config._otp = authReq.otp;
|
||||
|
||||
if (!state.config.token && state._can_pair) {
|
||||
console.info("");
|
||||
console.info("==============================================");
|
||||
console.info(" Hey, Listen! ");
|
||||
console.info("==============================================");
|
||||
console.info(" ");
|
||||
console.info(" GO CHECK YOUR EMAIL! ");
|
||||
console.info(" ");
|
||||
console.info(" DEVICE PAIR CODE: 0000 ".replace(/0000/g, state.config._otp));
|
||||
console.info(" ");
|
||||
console.info("==============================================");
|
||||
console.info("");
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
, connect: function (pretoken, next) {
|
||||
//console.log("[connect] Enabling Pairing Locally...");
|
||||
state.config.pretoken = pretoken;
|
||||
state._connecting = true;
|
||||
|
||||
// TODO use php-style object querification
|
||||
utils.putConfig('config', packConfig(state.config), function (err/*, body*/) {
|
||||
if (err) {
|
||||
state._error = err;
|
||||
console.error("Error while initializing config [connect]:");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.info("waiting...");
|
||||
next();
|
||||
});
|
||||
}
|
||||
, offer: function (token, next) {
|
||||
//console.log("[offer] Pairing Enabled by Relay");
|
||||
state.config.token = token;
|
||||
if (state._error) {
|
||||
return;
|
||||
}
|
||||
state._connecting = true;
|
||||
try {
|
||||
require('jsonwebtoken').decode(token);
|
||||
//console.log(require('jsonwebtoken').decode(token));
|
||||
} catch(e) {
|
||||
console.warn("[warning] could not decode token");
|
||||
}
|
||||
utils.putConfig('config', packConfig(state.config), function (err/*, body*/) {
|
||||
if (err) {
|
||||
state._error = err;
|
||||
console.error("Error while initializing config [offer]:");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
//console.log("Pairing Enabled Locally");
|
||||
next();
|
||||
});
|
||||
}
|
||||
, granted: function (_, next) {
|
||||
//console.log("[grant] Pairing complete!");
|
||||
next();
|
||||
}
|
||||
, end: function () {
|
||||
utils.putConfig('enable', [], function (err) {
|
||||
if (err) { console.error(err); return; }
|
||||
console.info("Success");
|
||||
|
||||
// workaround for https://github.com/nodejs/node/issues/21319
|
||||
if (state._useTty) {
|
||||
setTimeout(function () {
|
||||
console.info("Some fun things to try first:\n");
|
||||
console.info(" ~/telebit http 3000");
|
||||
console.info(" ~/telebit tcp 5050");
|
||||
console.info(" ~/telebit ssh auto");
|
||||
console.info();
|
||||
console.info("Press any key to continue...");
|
||||
console.info();
|
||||
process.exit(0);
|
||||
}, 0.5 * 1000);
|
||||
return;
|
||||
}
|
||||
// end workaround
|
||||
|
||||
parseCli(state);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseCli(/*state*/) {
|
||||
var special = [
|
||||
'false', 'none', 'off', 'disable'
|
||||
, 'true', 'auto', 'on', 'enable'
|
||||
];
|
||||
if (-1 !== argv.indexOf('init')) {
|
||||
utils.putConfig('list', []/*, function (err) {
|
||||
}*/);
|
||||
return;
|
||||
}
|
||||
|
||||
if ([ 'ssh', 'http', 'tcp' ].some(function (key) {
|
||||
if (key !== argv[0]) {
|
||||
return false;
|
||||
}
|
||||
if (argv[1]) {
|
||||
if (String(argv[1]) === String(parseInt(argv[1], 10))) {
|
||||
// looks like a port
|
||||
argv[1] = parseInt(argv[1], 10);
|
||||
} else if (/\/|\\/.test(argv[1])) {
|
||||
// looks like a path
|
||||
argv[1] = path.resolve(argv[1]);
|
||||
} else if (-1 === special.indexOf(argv[1])) {
|
||||
console.error("Not sure what you meant by '" + argv[1] + "'.");
|
||||
console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'");
|
||||
return true;
|
||||
}
|
||||
utils.putConfig(argv[0], argv.slice(1));
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
help();
|
||||
process.exit(11);
|
||||
}
|
||||
|
||||
function handleConfig(err, config) {
|
||||
//console.log('CONFIG');
|
||||
//console.log(config);
|
||||
state.config = config;
|
||||
|
||||
if (err) { console.error(err); process.exit(101); return; }
|
||||
|
||||
//
|
||||
// check for init first, before anything else
|
||||
// because it has arguments that may help in
|
||||
// the next steps
|
||||
//
|
||||
if (-1 !== argv.indexOf('init')) {
|
||||
parsers.init(argv, getToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.config.relay || !state.config.token) {
|
||||
if (!state.config.relay) {
|
||||
state.config.relay = 'telebit.cloud';
|
||||
}
|
||||
|
||||
//console.log("question the user?", Date.now());
|
||||
askForConfig(state, function (err, state) {
|
||||
// no errors actually get passed, so this is just future-proofing
|
||||
if (err) { throw err; }
|
||||
|
||||
if (!state.config.token && state._can_pair) {
|
||||
state.config._otp = common.otp();
|
||||
}
|
||||
|
||||
//console.log("done questioning:", Date.now());
|
||||
if (!state.token && !state.config.token) {
|
||||
getToken(err, state);
|
||||
} else {
|
||||
parseCli(state);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log("no questioning:");
|
||||
parseCli(state);
|
||||
}
|
||||
|
||||
function parseConfig(err, text) {
|
||||
|
||||
console.info("");
|
||||
console.info(verstr.join(' '));
|
||||
|
||||
try {
|
||||
state._clientConfig = JSON.parse(text || '{}');
|
||||
} catch(e1) {
|
||||
try {
|
||||
state._clientConfig = YAML.safeLoad(text || '{}');
|
||||
} catch(e2) {
|
||||
console.error(e1.message);
|
||||
console.error(e2.message);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
|
||||
common._init(
|
||||
// make a default working dir and log dir
|
||||
state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit')
|
||||
, (state._clientConfig.root && path.join(state._clientConfig.root, 'etc'))
|
||||
|| path.resolve(common.DEFAULT_CONFIG_PATH, '..')
|
||||
);
|
||||
state._ipc = common.pipename(state._clientConfig, true);
|
||||
|
||||
if (!Object.keys(state._clientConfig).length) {
|
||||
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
|
||||
console.info("");
|
||||
}
|
||||
|
||||
if ((err && 'ENOENT' === err.code) || !Object.keys(state._clientConfig).length) {
|
||||
if (!err || 'ENOENT' === err.code) {
|
||||
//console.warn("Empty config file. Run 'telebit init' to configure.\n");
|
||||
} else {
|
||||
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
utils.request({ service: 'config' }, handleConfig);
|
||||
}
|
||||
|
||||
var parsers = {
|
||||
init: function (argv, parseCb) {
|
||||
var answers = {};
|
||||
var boolish = [ '--advanced' ];
|
||||
if ('init' !== argv[0]) {
|
||||
throw new Error("init must be the first argument");
|
||||
}
|
||||
argv.shift();
|
||||
|
||||
// init --foo bar
|
||||
argv.forEach(function (arg, i) {
|
||||
if (!/^--/.test(arg)) { return; }
|
||||
if (-1 !== boolish.indexOf(arg)) {
|
||||
answers['_' + arg.replace(/^--/, '')] = true;
|
||||
}
|
||||
if (/^-/.test(argv[i + 1])) {
|
||||
throw new Error(argv[i + 1] + ' requires an argument');
|
||||
}
|
||||
answers[arg] = argv[i + 1];
|
||||
});
|
||||
|
||||
// init foo:bar
|
||||
argv.forEach(function (arg) {
|
||||
if (/^--/.test(arg)) { return; }
|
||||
var parts = arg.split(/:/g);
|
||||
if (2 !== parts.length) {
|
||||
throw new Error("bad option to init: '" + arg + "'");
|
||||
}
|
||||
if (answers[parts[0]]) {
|
||||
throw new Error("duplicate key to init '" + parts[0] + "'");
|
||||
}
|
||||
answers[parts[0]] = parts[1];
|
||||
});
|
||||
|
||||
// things that aren't straight-forward copy-over
|
||||
if (!answers.advanced && !answers.relay) {
|
||||
answers.relay = 'telebit.cloud';
|
||||
}
|
||||
if (Array.isArray(common._NOTIFICATIONS[answers.update])) {
|
||||
common._NOTIFICATIONS[answers.update].forEach(function (name) {
|
||||
state.config[name] = true;
|
||||
});
|
||||
}
|
||||
if (answers.servernames) {
|
||||
state.config._servernames = answers.servernames;
|
||||
}
|
||||
if (answers.ports) {
|
||||
state.config._ports = answers.ports;
|
||||
}
|
||||
|
||||
// things that are straight-forward copy-over
|
||||
common.CONFIG_KEYS.forEach(function (key) {
|
||||
if ('true' === answers[key]) { answers[key] = true; }
|
||||
if ('false' === answers[key]) { answers[key] = false; }
|
||||
if ('null' === answers[key]) { answers[key] = null; }
|
||||
if ('undefined' === answers[key]) { delete answers[key]; }
|
||||
if ('undefined' !== typeof answers[key]) {
|
||||
state.config[key] = answers[key];
|
||||
}
|
||||
});
|
||||
|
||||
askForConfig(state, function (err, state) {
|
||||
if (err) { parseCb(err); return; }
|
||||
|
||||
if (!state.config.token && state._can_pair) {
|
||||
state.config._otp = common.otp();
|
||||
}
|
||||
|
||||
argv.unshift('init');
|
||||
parseCb(null, state);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
require('fs').readFile(confpath, 'utf8', parseConfig);
|
||||
|
||||
}());
|
866
bin/telebitd.js
866
bin/telebitd.js
|
@ -1,866 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var pkg = require('../package.json');
|
||||
|
||||
var url = require('url');
|
||||
var path = require('path');
|
||||
var os = require('os');
|
||||
var fs = require('fs');
|
||||
var common = require('../lib/cli-common.js');
|
||||
var http = require('http');
|
||||
var YAML = require('js-yaml');
|
||||
var recase = require('recase').create({});
|
||||
var camelCopy = recase.camelCopy.bind(recase);
|
||||
var snakeCopy = recase.snakeCopy.bind(recase);
|
||||
var state = { homedir: os.homedir(), servernames: {}, ports: {} };
|
||||
|
||||
var argv = process.argv.slice(2);
|
||||
|
||||
var confIndex = argv.indexOf('--config');
|
||||
var confpath;
|
||||
var confargs;
|
||||
if (-1 === confIndex) {
|
||||
confIndex = argv.indexOf('-c');
|
||||
}
|
||||
if (-1 !== confIndex) {
|
||||
confargs = argv.splice(confIndex, 2);
|
||||
confpath = confargs[1];
|
||||
}
|
||||
|
||||
var cancelUpdater = require('../lib/updater')(pkg);
|
||||
|
||||
function help() {
|
||||
console.info('');
|
||||
console.info('Telebit Daemon v' + pkg.version);
|
||||
console.info('');
|
||||
console.info('Usage:');
|
||||
console.info('');
|
||||
console.info('\ttelebitd --config <path>');
|
||||
console.info('\tex: telebitd --config ~/.config/telebit/telebitd.yml');
|
||||
console.info('');
|
||||
console.info('');
|
||||
console.info('Config:');
|
||||
console.info('');
|
||||
console.info('\tSee https://git.coolaj86.com/coolaj86/telebit.js');
|
||||
console.info('');
|
||||
console.info('');
|
||||
}
|
||||
|
||||
var verstr = [ pkg.name + ' daemon v' + pkg.version ];
|
||||
if (-1 === confIndex) {
|
||||
// We have two possible valid paths if no --config is given (i.e. run from an npm-only install)
|
||||
// * {install}/etc/telebitd.yml
|
||||
// * ~/.config/telebit/telebitd.yml
|
||||
// We'll asume the later since the installers include --config in the system launcher script
|
||||
confpath = common.DEFAULT_CONFIG_PATH;
|
||||
verstr.push('(--config "' + confpath + '")');
|
||||
}
|
||||
|
||||
if (-1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) {
|
||||
help();
|
||||
process.exit(0);
|
||||
}
|
||||
if (!confpath || /^--/.test(confpath)) {
|
||||
help();
|
||||
process.exit(1);
|
||||
}
|
||||
var tokenpath = path.join(path.dirname(confpath), 'access_token.txt');
|
||||
var token;
|
||||
try {
|
||||
token = fs.readFileSync(tokenpath, 'ascii').trim();
|
||||
console.log('[DEBUG] access_token', typeof token, token);
|
||||
} catch(e) {
|
||||
// ignore
|
||||
}
|
||||
var controlServer;
|
||||
var tun;
|
||||
|
||||
var controllers = {};
|
||||
function saveConfig(cb) {
|
||||
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb);
|
||||
}
|
||||
function getServername(servernames, sub) {
|
||||
if (state.servernames[sub]) {
|
||||
return sub;
|
||||
}
|
||||
|
||||
var names = Object.keys(servernames).map(function (servername) {
|
||||
if ('*.' === servername.slice(0,2)) {
|
||||
return servername;
|
||||
}
|
||||
return '*.' + servername;
|
||||
}).sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
});
|
||||
|
||||
return names.filter(function (pattern) {
|
||||
// '.example.com' = '*.example.com'.split(1)
|
||||
var subPiece = pattern.slice(1);
|
||||
// '.com' = 'sub.example.com'.slice(-4)
|
||||
// '.example.com' = 'sub.example.com'.slice(-12)
|
||||
if (subPiece === sub.slice(-subPiece.length)) {
|
||||
return subPiece;
|
||||
}
|
||||
})[0];
|
||||
}
|
||||
controllers.http = function (req, res, opts) {
|
||||
function getAppname(pathname) {
|
||||
// port number
|
||||
if (String(pathname) === String(parseInt(pathname, 10))) {
|
||||
return String(pathname);
|
||||
}
|
||||
var paths = pathname.split(/[\\\/\:]/);
|
||||
// rid trailing slash(es)
|
||||
while (!paths[paths.length -1]) {
|
||||
paths.pop();
|
||||
}
|
||||
var name = paths.pop();
|
||||
name = path.basename(name, path.extname(name));
|
||||
name = name.replace(/\./, '-').replace(/-+/, '-');
|
||||
return name;
|
||||
}
|
||||
if (!opts.body) {
|
||||
res.statusCode = 422;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({"error":{"message":"module \'http\' needs more arguments"}}));
|
||||
return;
|
||||
}
|
||||
var active = true;
|
||||
var portOrPath = opts.body[0];
|
||||
var appname = getAppname(portOrPath);
|
||||
var subdomain = opts.body[1];
|
||||
var remoteHost;
|
||||
if (subdomain) {
|
||||
var handlerName = getServername(state.servernames, subdomain);
|
||||
if (!handlerName) {
|
||||
active = false;
|
||||
}
|
||||
if (!state.servernames[subdomain]) {
|
||||
state.servernames[subdomain] = {};
|
||||
}
|
||||
state.servernames[subdomain].handler = portOrPath;
|
||||
remoteHost = subdomain;
|
||||
} else {
|
||||
if (!Object.keys(state.servernames).sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
}).some(function (key) {
|
||||
if (state.servernames[key].handler === appname) {
|
||||
// example.com.handler: 3000 // already set
|
||||
remoteHost = key;
|
||||
return true;
|
||||
}
|
||||
if (state.servernames[key].wildcard) {
|
||||
if (!state.servernames[appname + '.' + key]) {
|
||||
state.servernames[appname + '.' + key] = {};
|
||||
}
|
||||
state.servernames[appname + '.' + key].handler = portOrPath;
|
||||
remoteHost = appname + '.' + key;
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
Object.keys(state.servernames).some(function (key) {
|
||||
state.servernames[key].handler = portOrPath;
|
||||
remoteHost = appname + '.' + key;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
state.config.servernames = state.servernames;
|
||||
saveConfig(function (err) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
success: true
|
||||
, active: active
|
||||
, remote: remoteHost
|
||||
, local: portOrPath
|
||||
, saved: !err
|
||||
, module: 'http'
|
||||
}));
|
||||
});
|
||||
};
|
||||
controllers.tcp = function (req, res, opts) {
|
||||
if (!opts.body) {
|
||||
res.statusCode = 422;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: { message: "module 'tcp' needs more arguments" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
var active;
|
||||
var remotePort = opts.body[1];
|
||||
var portOrPath = opts.body[0];
|
||||
|
||||
// portnum
|
||||
if (remotePort) {
|
||||
if (!state.ports[remotePort]) {
|
||||
active = false;
|
||||
return;
|
||||
}
|
||||
// forward-to port-or-module
|
||||
// TODO we can't send files over tcp until we fix the connect event bug
|
||||
state.ports[remotePort].handler = portOrPath;
|
||||
} else {
|
||||
if (!Object.keys(state.ports).some(function (key) {
|
||||
if (!state.ports[key].handler) {
|
||||
state.ports[key].handler = portOrPath;
|
||||
remotePort = key;
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
Object.keys(state.ports).some(function (key) {
|
||||
state.ports[key].handler = portOrPath;
|
||||
remotePort = key;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
state.config.ports = state.ports;
|
||||
saveConfig(function (err) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
success: true
|
||||
, active: active
|
||||
, remote: remotePort
|
||||
, local: portOrPath
|
||||
, saved: !err
|
||||
, module: 'tcp'
|
||||
}));
|
||||
});
|
||||
};
|
||||
controllers.ssh = function (req, res, opts) {
|
||||
if (!opts.body) {
|
||||
res.statusCode = 422;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({"error":{"message":"module 'ssh' needs more arguments"}}));
|
||||
return;
|
||||
}
|
||||
|
||||
function sshSuccess() {
|
||||
//state.config.sshAuto = state.sshAuto;
|
||||
saveConfig(function (err) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
success: true
|
||||
, active: true
|
||||
, remote: Object.keys(state.config.ports)[0]
|
||||
, local: state.config.sshAuto || 22
|
||||
, saved: !err
|
||||
, module: 'ssh'
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
var sshAuto = opts.body[0];
|
||||
if (-1 !== [ 'false', 'none', 'off', 'disable' ].indexOf(sshAuto)) {
|
||||
state.config.sshAuto = false;
|
||||
sshSuccess();
|
||||
return;
|
||||
}
|
||||
if (-1 !== [ 'true', 'auto', 'on', 'enable' ].indexOf(sshAuto)) {
|
||||
state.config.sshAuto = 22;
|
||||
sshSuccess();
|
||||
return;
|
||||
}
|
||||
sshAuto = parseInt(sshAuto, 10);
|
||||
if (!sshAuto || sshAuto <= 0 || sshAuto > 65535) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: { message: "bad ssh_auto option '" + opts.body[0] + "'" } }));
|
||||
return;
|
||||
}
|
||||
state.config.sshAuto = sshAuto;
|
||||
sshSuccess();
|
||||
};
|
||||
function serveControlsHelper() {
|
||||
controlServer = http.createServer(function (req, res) {
|
||||
var opts = url.parse(req.url, true);
|
||||
if (opts.query._body) {
|
||||
try {
|
||||
opts.body = JSON.parse(decodeURIComponent(opts.query._body, true));
|
||||
} catch(e) {
|
||||
res.statusCode = 500;
|
||||
res.end('{"error":{"message":"?_body={{bad_format}}"}}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function listSuccess() {
|
||||
var dumpy = {
|
||||
servernames: state.servernames
|
||||
, ports: state.ports
|
||||
, ssh: state.config.sshAuto || 'disabled'
|
||||
, code: 'CONFIG'
|
||||
};
|
||||
if (state.otp) {
|
||||
dumpy.device_pair_code = state.otp;
|
||||
}
|
||||
|
||||
if (state._can_pair && state.config.email && !state.token) {
|
||||
dumpy.code = "AWAIT_AUTH";
|
||||
dumpy.message = "Please run 'telebit init' to authenticate.";
|
||||
}
|
||||
|
||||
res.end(JSON.stringify(dumpy));
|
||||
}
|
||||
|
||||
if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) {
|
||||
res.setHeader('Content-Type', 'appliCation/json');
|
||||
res.end(JSON.stringify(state.config));
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// without proper config
|
||||
//
|
||||
function saveAndReport(err, _tun) {
|
||||
console.log('[DEBUG] saveAndReport config write', confpath);
|
||||
console.log(YAML.safeDump(snakeCopy(state.config)));
|
||||
if (err) { throw err; }
|
||||
tun = _tun;
|
||||
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end('{"error":{"message":"Could not save config file after init: ' + err.message.replace(/"/g, "'")
|
||||
+ '.\nPerhaps check that the file exists and your user has permissions to write it?"}}');
|
||||
return;
|
||||
}
|
||||
|
||||
listSuccess();
|
||||
});
|
||||
}
|
||||
if (/\b(init|config)\b/.test(opts.pathname)) {
|
||||
var conf = {};
|
||||
if (!opts.body) {
|
||||
res.statusCode = 422;
|
||||
res.end('{"error":{"message":"module \'init\' needs more arguments"}}');
|
||||
return;
|
||||
}
|
||||
// relay, email, agree_tos, servernames, ports
|
||||
//
|
||||
opts.body.forEach(function (opt) {
|
||||
var parts = opt.split(/:/);
|
||||
if ('true' === parts[1]) {
|
||||
parts[1] = true;
|
||||
} else if ('false' === parts[1]) {
|
||||
parts[1] = false;
|
||||
} else if ('null' === parts[1]) {
|
||||
parts[1] = null;
|
||||
} else if ('undefined' === parts[1]) {
|
||||
parts[1] = undefined;
|
||||
}
|
||||
conf[parts[0]] = parts[1];
|
||||
});
|
||||
|
||||
// TODO camelCase query
|
||||
state.config.email = conf.email || state.config.email || '';
|
||||
if ('undefined' !== typeof conf.agreeTos
|
||||
|| 'undefined' !== typeof conf.agreeTos ) {
|
||||
state.config.agreeTos = conf.agreeTos || conf.agree_tos;
|
||||
}
|
||||
state.otp = conf._otp; // this should only be done on the client side
|
||||
state.config.relay = conf.relay || state.config.relay || '';
|
||||
console.log();
|
||||
console.log('conf.token', typeof conf.token, conf.token);
|
||||
console.log('state.config.token', typeof state.config.token, state.config.token);
|
||||
state.config.token = conf.token || state.config.token || null;
|
||||
state.config.secret = conf.secret || state.config.secret || null;
|
||||
state.pretoken = conf.pretoken || state.config.pretoken || null;
|
||||
if (state.secret) {
|
||||
console.log('state.secret');
|
||||
state.token = common.signToken(state);
|
||||
}
|
||||
if (!state.token) {
|
||||
console.log('!state.token');
|
||||
state.token = conf._token;
|
||||
}
|
||||
console.log();
|
||||
console.log('JSON.stringify(conf)');
|
||||
console.log(JSON.stringify(conf));
|
||||
console.log();
|
||||
console.log('JSON.stringify(state)');
|
||||
console.log(JSON.stringify(state));
|
||||
console.log();
|
||||
if ('undefined' !== typeof conf.newsletter) {
|
||||
state.config.newsletter = conf.newsletter;
|
||||
}
|
||||
if ('undefined' !== typeof conf.communityMember
|
||||
|| 'undefined' !== typeof conf.community_member) {
|
||||
state.config.communityMember = conf.communityMember || conf.community_member;
|
||||
}
|
||||
if ('undefined' !== typeof conf.telemetry) {
|
||||
state.config.telemetry = conf.telemetry;
|
||||
}
|
||||
if (conf._servernames) {
|
||||
(conf._servernames||'').split(/,/g).forEach(function (key) {
|
||||
if (!state.config.servernames[key]) {
|
||||
state.config.servernames[key] = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
if (conf._ports) {
|
||||
(conf._ports||'').split(/,/g).forEach(function (key) {
|
||||
if (!state.config.ports[key]) {
|
||||
state.config.ports[key] = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!state.config.relay || !state.config.email || !state.config.agreeTos) {
|
||||
console.log('aborting for some reason');
|
||||
res.statusCode = 400;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
code: "E_INIT"
|
||||
, message: "Missing important config file params"
|
||||
, _params: JSON.stringify(conf)
|
||||
, _config: JSON.stringify(state.config)
|
||||
, _body: JSON.stringify(opts.body)
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tun) {
|
||||
console.log('ending existing tunnel, starting anew');
|
||||
tun.end(function () {
|
||||
console.log('success ending');
|
||||
rawTunnel(saveAndReport);
|
||||
});
|
||||
tun = null;
|
||||
setTimeout(function () {
|
||||
if (!tun) {
|
||||
console.log('failed to end, but starting anyway');
|
||||
rawTunnel(saveAndReport);
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
console.log('no tunnel, starting anew');
|
||||
rawTunnel(saveAndReport);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (/restart/.test(opts.pathname)) {
|
||||
tun.end();
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
controlServer.close(function () {
|
||||
// TODO closeAll other things
|
||||
process.nextTick(function () {
|
||||
// system daemon will restart the process
|
||||
process.exit(22); // use non-success exit code
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Check for proper config
|
||||
//
|
||||
if (!state.config.relay || !state.config.email || !state.config.agreeTos) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: { code: "E_CONFIG", message: "Invalid config file. Please run 'telebit init'" }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// With proper config
|
||||
//
|
||||
if (/http/.test(opts.pathname)) {
|
||||
controllers.http(req, res, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/tcp/.test(opts.pathname)) {
|
||||
controllers.tcp(req, res, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/save|commit/.test(opts.pathname)) {
|
||||
state.config.servernames = state.servernames;
|
||||
state.config.ports = state.ports;
|
||||
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
"error":{"message":"Could not save config file. Perhaps you're not running as root?"}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
listSuccess();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (/ssh/.test(opts.pathname)) {
|
||||
controllers.ssh(req, res, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/enable/.test(opts.pathname)) {
|
||||
delete state.config.disable;// = undefined;
|
||||
if (tun) {
|
||||
listSuccess();
|
||||
return;
|
||||
}
|
||||
rawTunnel(function (err, _tun) {
|
||||
if (err) { throw err; }
|
||||
tun = _tun;
|
||||
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: { message: "Could not save config file. Perhaps you're user doesn't have permission?" }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
listSuccess();
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (/disable/.test(opts.pathname)) {
|
||||
state.config.disable = true;
|
||||
if (tun) { tun.end(); tun = null; }
|
||||
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
res.end(JSON.stringify({
|
||||
"error":{"message":"Could not save config file. Perhaps you're not running as root?"}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
res.end('{"success":true}');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (/status/.test(opts.pathname)) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify(
|
||||
{ status: (state.config.disable ? 'disabled' : 'enabled')
|
||||
, ready: ((state.config.relay && (state.config.token || state.config.agreeTos)) ? true : false)
|
||||
}
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (/list/.test(opts.pathname)) {
|
||||
listSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}}));
|
||||
});
|
||||
if (fs.existsSync(state._ipc.path)) {
|
||||
fs.unlinkSync(state._ipc.path);
|
||||
}
|
||||
// mask is so that processes owned by other users
|
||||
// can speak to this process, which is probably root-owned
|
||||
var oldUmask = process.umask(0x0000);
|
||||
var serverOpts = {
|
||||
writableAll: true
|
||||
, readableAll: true
|
||||
, exclusive: false
|
||||
};
|
||||
if ('socket' === state._ipc.type) {
|
||||
require('mkdirp').sync(path.dirname(state._ipc.path));
|
||||
}
|
||||
// https://nodejs.org/api/net.html#net_server_listen_options_callback
|
||||
// path is ignore if port is defined
|
||||
// https://git.coolaj86.com/coolaj86/telebit.js/issues/23#issuecomment-326
|
||||
if (state._ipc.port) {
|
||||
serverOpts.host = 'localhost';
|
||||
serverOpts.port = state._ipc.port;
|
||||
} else {
|
||||
serverOpts.path = state._ipc.path;
|
||||
}
|
||||
controlServer.listen(serverOpts, function () {
|
||||
process.umask(oldUmask);
|
||||
var address = this.address();
|
||||
if (address.port) {
|
||||
common.setPort(state.config, address.port);
|
||||
}
|
||||
//console.log(this.address());
|
||||
console.info("[info] Listening for commands on", address);
|
||||
});
|
||||
}
|
||||
|
||||
function serveControls() {
|
||||
if (state.config.disable) {
|
||||
console.info("[info] starting disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.config.relay && (state.config.token || state.config.pretoken)) {
|
||||
console.info("[info] connecting with stored token");
|
||||
rawTunnel(function (err, _tun) {
|
||||
if (err) { throw err; }
|
||||
if (_tun) { tun = _tun; }
|
||||
setTimeout(function () {
|
||||
// TODO attach handler to tunnel
|
||||
serveControlsHelper();
|
||||
}, 150);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
console.info("[info] waiting for init/authentication (missing relay and/or token)");
|
||||
}
|
||||
|
||||
serveControlsHelper();
|
||||
}
|
||||
|
||||
function parseConfig(err, text) {
|
||||
|
||||
function run() {
|
||||
if (!state.config) {
|
||||
state.config = {};
|
||||
}
|
||||
common._init(
|
||||
state.config.root || path.join(os.homedir(), '.local/share/telebit') // || path.join(__dirname, '..')
|
||||
, (state.config.root && path.join(state.config.root, 'etc')) || path.resolve(common.DEFAULT_CONFIG_PATH, '..')
|
||||
);
|
||||
state._ipc = common.pipename(state.config, true);
|
||||
console.info('');
|
||||
console.info(verstr.join(' '));
|
||||
if (!state.config.sock) {
|
||||
console.info('(' + state._ipc.comment + ': "' + state._ipc.path + '")');
|
||||
}
|
||||
console.info('');
|
||||
state.token = state.token || state.config.token || token;
|
||||
state.pretoken = state.pretoken || state.config.pretoken;
|
||||
|
||||
state._confpath = confpath;
|
||||
if (!state.config.servernames) {
|
||||
state.config.servernames = {};
|
||||
}
|
||||
if (!state.config.ports) {
|
||||
state.config.ports = {};
|
||||
}
|
||||
state.servernames = JSON.parse(JSON.stringify(state.config.servernames));
|
||||
state.ports = JSON.parse(JSON.stringify(state.config.ports));
|
||||
|
||||
serveControls();
|
||||
}
|
||||
|
||||
try {
|
||||
state.config = JSON.parse(text || '{}');
|
||||
} catch(e1) {
|
||||
try {
|
||||
state.config = YAML.safeLoad(text || '{}');
|
||||
} catch(e2) {
|
||||
console.error(e1.message);
|
||||
console.error(e2.message);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state.config = camelCopy(state.config || {}) || {};
|
||||
|
||||
run();
|
||||
|
||||
if ((err && 'ENOENT' === err.code) || !Object.keys(state.config).length) {
|
||||
if (!err || 'ENOENT' === err.code) {
|
||||
console.warn("Empty config file. Run 'telebit init' to configure.\n");
|
||||
} else {
|
||||
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rawTunnel(rawCb) {
|
||||
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
|
||||
rawCb(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
state.relay = state.config.relay;
|
||||
if (!state.relay) {
|
||||
rawCb(new Error("'" + state._confpath + "' is missing 'relay'"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(state.token || state.pretoken)) {
|
||||
rawCb(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tun) {
|
||||
rawCb(null, tun);
|
||||
return;
|
||||
}
|
||||
|
||||
common.api.wss(state, function (err, wss) {
|
||||
if (err) { rawCb(err); return; }
|
||||
state.wss = wss;
|
||||
|
||||
// Saves the token
|
||||
// state.handlers.access_token({ jwt: token });
|
||||
// Adds the token to the connection
|
||||
// tun.append(token);
|
||||
|
||||
state.greenlockConf = state.config.greenlock || {};
|
||||
state.sortingHat = state.config.sortingHat;
|
||||
|
||||
// TODO sortingHat.print(); ?
|
||||
// TODO Check undefined vs false for greenlock config
|
||||
var remote = require('../');
|
||||
|
||||
state.greenlockConfig = {
|
||||
version: state.greenlockConf.version || 'draft-11'
|
||||
, server: state.greenlockConf.server || 'https://acme-v02.api.letsencrypt.org/directory'
|
||||
, communityMember: state.greenlockConf.communityMember || state.config.communityMember
|
||||
, telemetry: state.greenlockConf.telemetry || state.config.telemetry
|
||||
, configDir: state.greenlockConf.configDir
|
||||
|| (state.config.root && path.join(state.config.root, 'etc/acme'))
|
||||
|| path.join(os.homedir(), '.config/telebit/acme')
|
||||
// TODO, store: require(state.greenlockConf.store.name || 'le-store-certbot').create(state.greenlockConf.store.options || {})
|
||||
, approveDomains: function (opts, certs, cb) {
|
||||
// Certs being renewed are listed in certs.altnames
|
||||
if (certs) {
|
||||
opts.domains = certs.altnames;
|
||||
cb(null, { options: opts, certs: certs });
|
||||
return;
|
||||
}
|
||||
|
||||
// Even though it's being tunneled by a trusted source
|
||||
// we need to make sure we don't get rate-limit spammed
|
||||
// with wildcard domains
|
||||
// TODO: finish implementing dynamic dns for wildcard certs
|
||||
if (getServername(state.servernames, opts.domains[0])) {
|
||||
opts.email = state.greenlockConf.email || state.config.email;
|
||||
opts.agreeTos = state.greenlockConf.agree || state.greenlockConf.agreeTos || state.config.agreeTos;
|
||||
cb(null, { options: opts, certs: certs });
|
||||
return;
|
||||
}
|
||||
|
||||
//cb(new Error("servername not found in allowed list"));
|
||||
}
|
||||
};
|
||||
state.insecure = state.config.relay_ignore_invalid_certificates;
|
||||
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
|
||||
|
||||
console.log("[DEBUG] token", typeof token, token);
|
||||
tun = remote.connect({
|
||||
relay: state.relay
|
||||
, wss: state.wss
|
||||
, config: state.config
|
||||
, otp: state.otp
|
||||
, sortingHat: state.sortingHat
|
||||
, net: state.net
|
||||
, insecure: state.insecure
|
||||
, token: state.token || state.pretoken // instance
|
||||
, servernames: state.servernames
|
||||
, ports: state.ports
|
||||
, handlers: state.handlers
|
||||
, greenlockConfig: state.greenlockConfig
|
||||
});
|
||||
|
||||
rawCb(null, tun);
|
||||
});
|
||||
}
|
||||
|
||||
state.handlers = {
|
||||
grant: function (grants) {
|
||||
console.info("");
|
||||
console.info("Connect to your device by any of the following means:");
|
||||
console.info("");
|
||||
grants.forEach(function (arr) {
|
||||
if ('https' === arr[0]) {
|
||||
if (!state.servernames[arr[1]]) {
|
||||
state.servernames[arr[1]] = {};
|
||||
}
|
||||
state.servernames[arr[1]].wildcard = true;
|
||||
} else if ('tcp' === arr[0]) {
|
||||
if (!state.ports[arr[2]]) {
|
||||
state.ports[arr[2]] = {};
|
||||
}
|
||||
}
|
||||
|
||||
if ('ssh+https' === arr[0]) {
|
||||
console.info("SSH+HTTPS");
|
||||
} else if ('ssh' === arr[0]) {
|
||||
console.info("SSH");
|
||||
} else if ('tcp' === arr[0]) {
|
||||
console.info("TCP");
|
||||
} else if ('https' === arr[0]) {
|
||||
console.info("HTTPS");
|
||||
}
|
||||
console.info('\t' + arr[0] + '://' + arr[1] + (arr[2] ? (':' + arr[2]) : ''));
|
||||
if ('ssh+https' === arr[0]) {
|
||||
console.info("\tex: ssh -o ProxyCommand='openssl s_client -connect %h:%p -servername %h -quiet' " + arr[1] + " -p 443\n");
|
||||
} else if ('ssh' === arr[0]) {
|
||||
console.info("\tex: ssh " + arr[1] + " -p " + arr[2] + "\n");
|
||||
} else if ('tcp' === arr[0]) {
|
||||
console.info("\tex: netcat " + arr[1] + " " + arr[2] + "\n");
|
||||
} else if ('https' === arr[0]) {
|
||||
console.info("\tex: curl https://" + arr[1] + "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
, access_token: function (opts) {
|
||||
if ('undefined' === opts.jwt || !opts.jwt) {
|
||||
console.error("Granted empty access token... ??");
|
||||
console.error(JSON.stringify(opts));
|
||||
return;
|
||||
}
|
||||
state.token = opts.jwt || opts.access_token;
|
||||
state.config.token = opts.jwt || opts.access_token;
|
||||
console.info("Updating '" + tokenpath + "' with new token:");
|
||||
try {
|
||||
fs.writeFileSync(tokenpath, opts.jwt);
|
||||
fs.writeFileSync(confpath, YAML.safeDump(snakeCopy(state.config)));
|
||||
} catch (e) {
|
||||
console.error("Token not saved:");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function sigHandler() {
|
||||
console.info('Received kill signal. Attempting to exit cleanly...');
|
||||
|
||||
// We want to handle cleanup properly unless something is broken in our cleanup process
|
||||
// that prevents us from exitting, in which case we want the user to be able to send
|
||||
// the signal again and exit the way it normally would.
|
||||
process.removeListener('SIGINT', sigHandler);
|
||||
if (tun) {
|
||||
tun.end();
|
||||
}
|
||||
if (controlServer) {
|
||||
controlServer.close();
|
||||
}
|
||||
cancelUpdater();
|
||||
}
|
||||
// reverse 2FA otp
|
||||
|
||||
process.on('SIGINT', sigHandler);
|
||||
|
||||
state.net = state.net || {
|
||||
createConnection: function (info, cb) {
|
||||
// data is the hello packet / first chunk
|
||||
// info = { data, servername, port, host, remoteFamily, remoteAddress, remotePort }
|
||||
var net = require('net');
|
||||
// socket = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
|
||||
var socket = net.createConnection({ port: info.port, host: info.host }, cb);
|
||||
return socket;
|
||||
}
|
||||
};
|
||||
|
||||
fs.readFile(confpath, 'utf8', parseConfig);
|
||||
|
||||
}());
|
|
@ -1,20 +0,0 @@
|
|||
email: 'jon@example.com' # must be valid (for certificate recovery and security alerts)
|
||||
agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
|
||||
community_member: true # receive infrequent relevant updates
|
||||
telemetry: true # contribute to project telemetric data
|
||||
relay: telebit.cloud # Which Telebit Relay to use
|
||||
#secret: '' # Shared Secret with Telebit Relay for authorization
|
||||
#token: '' # Token created by Telebit Relay for authorization
|
||||
ssh_auto: 22 # forward ssh-looking packets, from any connection, to port 22
|
||||
servernames: # hostnames that direct to the Telebit Relay admin console
|
||||
example.com:
|
||||
handler: 3000
|
||||
example.net:
|
||||
handler: /path/to/module
|
||||
ports:
|
||||
5050:
|
||||
handler: 54321
|
||||
greenlock:
|
||||
version: 'draft-11'
|
||||
server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||
config_dir: '/opt/telebit/etc/acme.staging/'
|
|
@ -1,7 +0,0 @@
|
|||
email: 'jon@example.com' # must be valid (for certificate recovery and security alerts)
|
||||
agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
|
||||
community_member: true # receive infrequent relevant updates
|
||||
telemetry: true # contribute to project telemetric data
|
||||
relay: telebit.cloud # Which Telebit Relay to use
|
||||
#secret: '' # Shared Secret with Telebit Relay for authorization
|
||||
#token: '' # Token created by Telebit Relay for authorization
|
|
@ -1,310 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.debug = (-1 !== (process.env.NODE_DEBUG||'').split(/\s+/g).indexOf('telebit'));
|
||||
var common = module.exports;
|
||||
|
||||
var path = require('path');
|
||||
var url = require('url');
|
||||
var fs = require('fs');
|
||||
var mkdirp = require('mkdirp');
|
||||
var os = require('os');
|
||||
var homedir = os.homedir();
|
||||
var urequest = require('@coolaj86/urequest');
|
||||
|
||||
common._NOTIFICATIONS = {
|
||||
'newsletter': [ 'newsletter', 'communityMember' ]
|
||||
, 'important': [ 'communityMember' ]
|
||||
};
|
||||
common.CONFIG_KEYS = [
|
||||
'newsletter'
|
||||
, 'communityMember'
|
||||
, 'telemetry'
|
||||
, 'sshAuto'
|
||||
, 'email'
|
||||
, 'agreeTos'
|
||||
, 'relay'
|
||||
, 'token'
|
||||
, 'pretoken'
|
||||
, 'secret'
|
||||
];
|
||||
//, '_servernames' // list instead of object
|
||||
//, '_ports' // list instead of object
|
||||
//, '_otp' // otp should not be saved
|
||||
//, '_token' // temporary token
|
||||
|
||||
common.getPort = function (config, cb) {
|
||||
var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
|
||||
if (cb) {
|
||||
return fs.readFile(portfile, 'utf8', function (err, text) {
|
||||
cb(err, parseInt((text||'').trim(), 10) || null);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
return parseInt(fs.readFileSync(portfile, 'utf8').trim(), 10) || null;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
common.setPort = function (config, num, cb) {
|
||||
var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
|
||||
var numstr = (num || '').toString();
|
||||
if (cb) {
|
||||
return fs.writeFile(portfile, numstr, 'utf8', function (err) {
|
||||
cb(err);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
return fs.writeFileSync(portfile, numstr, 'utf8');
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
common.removePort = function (config, cb) {
|
||||
var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
|
||||
if (cb) {
|
||||
return fs.unlink(portfile, function (err, text) {
|
||||
cb(err, (text||'').trim());
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
return fs.unlinkSync(portfile);
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
common.pipename = function (config) {
|
||||
var _ipc = {
|
||||
path: (config.sock || common.DEFAULT_SOCK_PATH)
|
||||
, comment: (/^win/i.test(os.platform()) ? 'windows pipe' : 'unix socket')
|
||||
, type: (/^win/i.test(os.platform()) ? 'pipe' : 'socket')
|
||||
};
|
||||
if ('pipe' === _ipc.type) {
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/ipc/pipe-names
|
||||
// Allows all characters accept backslash as part of the name
|
||||
_ipc.path = '\\\\.\\pipe\\' + _ipc.path.replace(/\\/g, '/');
|
||||
}
|
||||
return _ipc;
|
||||
};
|
||||
common.DEFAULT_SOCK_PATH = path.join(homedir, '.local/share/telebit/var/run', 'telebit.sock');
|
||||
common.DEFAULT_CONFIG_PATH = path.join(homedir, '.config/telebit', 'telebitd.yml');
|
||||
|
||||
common.parseUrl = function (hostname) {
|
||||
var location = url.parse(hostname);
|
||||
if (!location.protocol || /\./.test(location.protocol)) {
|
||||
hostname = 'https://' + hostname;
|
||||
location = url.parse(hostname);
|
||||
}
|
||||
hostname = location.hostname + (location.port ? ':' + location.port : '');
|
||||
hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
|
||||
return hostname;
|
||||
};
|
||||
common.parseHostname = function (hostname) {
|
||||
var location = url.parse(hostname);
|
||||
if (!location.protocol || /\./.test(location.protocol)) {
|
||||
hostname = 'https://' + hostname;
|
||||
location = url.parse(hostname);
|
||||
}
|
||||
//hostname = location.hostname + (location.port ? ':' + location.port : '');
|
||||
//hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
|
||||
return location.hostname;
|
||||
};
|
||||
|
||||
common.apiDirectory = '_apis/telebit.cloud/index.json';
|
||||
|
||||
common.otp = function getOtp() {
|
||||
return Math.round(Math.random() * 9999).toString().padStart(4, '0');
|
||||
};
|
||||
common.signToken = function (state) {
|
||||
var jwt = require('jsonwebtoken');
|
||||
var tokenData = {
|
||||
domains: Object.keys(state.config.servernames || {}).filter(function (name) {
|
||||
return /\./.test(name);
|
||||
})
|
||||
, ports: Object.keys(state.config.ports || {}).filter(function (port) {
|
||||
port = parseInt(port, 10);
|
||||
return port > 0 && port <= 65535;
|
||||
})
|
||||
, aud: state._relayUrl
|
||||
, iss: Math.round(Date.now() / 1000)
|
||||
};
|
||||
|
||||
return jwt.sign(tokenData, state.config.secret);
|
||||
};
|
||||
common.api = {};
|
||||
common.api.directory = function (state, next) {
|
||||
state._relayUrl = common.parseUrl(state.relay);
|
||||
urequest({ url: state._relayUrl + common.apiDirectory, json: true }, function (err, resp, dir) {
|
||||
if (!dir) { dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; }
|
||||
state._apiDirectory = dir;
|
||||
next(err, dir);
|
||||
});
|
||||
};
|
||||
common.api._parseWss = function (state, dir) {
|
||||
if (!dir || !dir.api_host) {
|
||||
dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
|
||||
}
|
||||
state._relayHostname = common.parseHostname(state.relay);
|
||||
return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname;
|
||||
};
|
||||
common.api.wss = function (state, cb) {
|
||||
common.api.directory(state, function (err, dir) {
|
||||
cb(err, common.api._parseWss(state, dir));
|
||||
});
|
||||
};
|
||||
common.api.token = function (state, handlers) {
|
||||
common.api.directory(state, function (err, dir) {
|
||||
// directory, requested, connect, tunnelUrl, offer, granted, end
|
||||
function afterDir() {
|
||||
if (common.debug) { console.log('[debug] after dir'); }
|
||||
state.wss = common.api._parseWss(state, dir);
|
||||
|
||||
handlers.tunnelUrl(state.wss, function () {
|
||||
if (common.debug) { console.log('[debug] after tunnelUrl'); }
|
||||
if (state.config.secret /* && !state.config.token */) {
|
||||
state.config._token = common.signToken(state);
|
||||
}
|
||||
state.token = state.token || state.config.token || state.config._token;
|
||||
if (state.token) {
|
||||
if (common.debug) { console.log('[debug] token via token or secret'); }
|
||||
// { token, pretoken }
|
||||
handlers.connect(state.token, function () {
|
||||
handlers.end(null, function () {});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// backwards compat (TODO remove)
|
||||
if (err || !dir || !dir.pair_request) {
|
||||
if (common.debug) { console.log('[debug] no dir, connect'); }
|
||||
handlers.error(new Error("No token found or generated, and no pair_request api found."));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO sign token with own private key, including public key and thumbprint
|
||||
// (much like ACME JOSE account)
|
||||
var otp = state.config._otp; // common.otp();
|
||||
var authReq = {
|
||||
subject: state.config.email
|
||||
, subject_scheme: 'mailto'
|
||||
// TODO create domains list earlier
|
||||
, scope: (state.config._servernames || Object.keys(state.config.servernames || {}))
|
||||
.concat(state.config._ports || Object.keys(state.config.ports || {})).join(',')
|
||||
, otp: otp
|
||||
, hostname: os.hostname()
|
||||
// Used for User-Agent
|
||||
, os_type: os.type()
|
||||
, os_platform: os.platform()
|
||||
, os_release: os.release()
|
||||
, os_arch: os.arch()
|
||||
};
|
||||
var pairRequestUrl = url.resolve('https://' + dir.api_host.replace(/:hostname/g, state._relayHostname), dir.pair_request.pathname);
|
||||
var req = {
|
||||
url: pairRequestUrl
|
||||
, method: dir.pair_request.method
|
||||
, json: authReq
|
||||
};
|
||||
var firstReq = true;
|
||||
var firstReady = true;
|
||||
|
||||
function gotoNext(req) {
|
||||
if (common.debug) { console.log('[debug] gotoNext called'); }
|
||||
if (common.debug) { console.log(req); }
|
||||
urequest(req, function (err, resp, body) {
|
||||
if (err) {
|
||||
if (common.debug) { console.log('[debug] gotoNext error'); }
|
||||
err._request = req;
|
||||
err._hint = '[telebitd.js] pair request';
|
||||
handlers.error(err, function () {});
|
||||
return;
|
||||
}
|
||||
|
||||
function checkLocation() {
|
||||
if (common.debug) { console.log('[debug] checkLocation'); }
|
||||
if (common.debug) { console.log(body); }
|
||||
// pending, try again
|
||||
if ('pending' === body.status && resp.headers.location) {
|
||||
if (common.debug) { console.log('[debug] pending'); }
|
||||
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if ('ready' === body.status) {
|
||||
if (common.debug) { console.log('[debug] ready'); }
|
||||
if (firstReady) {
|
||||
if (common.debug) { console.log('[debug] first ready'); }
|
||||
firstReady = false;
|
||||
state.token = body.access_token;
|
||||
state.config.token = state.token;
|
||||
handlers.offer(body.access_token, function () {
|
||||
/*ignore*/
|
||||
});
|
||||
}
|
||||
setTimeout(gotoNext, 2 * 1000, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('complete' === body.status) {
|
||||
if (common.debug) { console.log('[debug] complete'); }
|
||||
handlers.granted(null, function () {
|
||||
handlers.end(null, function () {});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (common.debug) { console.log('[debug] bad status'); }
|
||||
var err = new Error("Bad State:" + body.status);
|
||||
err._request = req;
|
||||
handlers.error(err, function () {});
|
||||
}
|
||||
|
||||
if (firstReq) {
|
||||
if (common.debug) { console.log('[debug] first req'); }
|
||||
handlers.requested(authReq, function () {
|
||||
handlers.connect(body.access_token || body.jwt, function () {
|
||||
var err;
|
||||
if (!resp.headers.location) {
|
||||
err = new Error("bad authentication request response");
|
||||
err._resp = resp.toJSON();
|
||||
handlers.error(err, function () {});
|
||||
return;
|
||||
}
|
||||
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
|
||||
});
|
||||
});
|
||||
firstReq = false;
|
||||
return;
|
||||
} else {
|
||||
if (common.debug) { console.log('[debug] other req'); }
|
||||
checkLocation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
gotoNext(req);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if (dir && dir.api_host) {
|
||||
handlers.directory(dir, afterDir);
|
||||
} else {
|
||||
// backwards compat
|
||||
dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
|
||||
afterDir();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
common._init = function (rootpath, confpath) {
|
||||
try {
|
||||
mkdirp.sync(path.join(rootpath, 'var', 'log'));
|
||||
mkdirp.sync(path.join(rootpath, 'var', 'run'));
|
||||
mkdirp.sync(path.join(confpath));
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function (opts) {
|
||||
console.log("Could not connect");
|
||||
var socket = opts.socket;
|
||||
var handler = opts.handler;
|
||||
var http = require('http');
|
||||
var server = http.createServer(function (req, res) {
|
||||
console.log('responding to thing');
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end("<html>"
|
||||
+ "<head><title>Couldn't Connect</title></head>"
|
||||
+ "<body>Could not connect to localhost:" + handler + "</body>"
|
||||
+ "</html>");
|
||||
});
|
||||
//server.emit('connection', socket);
|
||||
socket.end("Could not connect to localhost:" + handler);
|
||||
};
|
Binary file not shown.
Before Width: | Height: | Size: 43 B |
|
@ -1,42 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Telebit</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script>document.body.hidden = true;</script>
|
||||
|
||||
<h1>Welcome Home <!-- as in 127.0.0.1, y'know ;) --></h1>
|
||||
<p>Go ahead and bookmark this page. It's yours now.</p>
|
||||
|
||||
<div>
|
||||
<h2>You've claimed <span class="js-servername">{{servername}}</span></h2>
|
||||
<p>Here's some ways you can use it:</p>
|
||||
<pre><code>
|
||||
telebit http 3000 # forward all https traffic to localhost:3000
|
||||
telebit http /path/to/module # handle incoming https traffic with a node module
|
||||
telebit http none # remove all https handlers</code></pre>
|
||||
</div>
|
||||
<p>You can <em>always</em> use this port for <strong>SSH over HTTPS</strong>, even while you're using it for something else:</p>
|
||||
<pre><code>
|
||||
ssh -o ProxyCommand='openssl s_client -connect %h:443 -servername %h -quiet' <span class="js-servername">{{servername}}</span></code></pre>
|
||||
|
||||
|
||||
<div class="js-port" hidden>
|
||||
<h2>You've claimed port <span class="js-serviceport">{{serviceport}}</span></h2>
|
||||
<p>Here's some ways you can use it:</p>
|
||||
<pre><code>
|
||||
telebit tcp 3000 # forward all tcp traffic to localhost:3000
|
||||
telebit tcp /path/to/module # handle incoming tcp traffic with a node module
|
||||
telebit tcp none # remove all tcp handlers</code></pre>
|
||||
</div>
|
||||
<p>You can <em>always</em> use this port for <strong>SSH</strong>, even while you're using it for something else:</p>
|
||||
<pre><code>telebit ssh 22
|
||||
|
||||
ssh <span class="js-servername">{{servername}}</span> -p <span class="js-serviceport">{{serviceport}}</span></code></pre>
|
||||
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,49 +0,0 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
document.body.hidden = false;
|
||||
|
||||
var hash = window.location.hash.substr(1);
|
||||
var query = window.location.search;
|
||||
|
||||
function parseQuery(search) {
|
||||
var args = search.substring(1).split('&');
|
||||
var argsParsed = {};
|
||||
var i, arg, kvp, key, value;
|
||||
|
||||
for (i=0; i < args.length; i++) {
|
||||
|
||||
arg = args[i];
|
||||
|
||||
if (-1 === arg.indexOf('=')) {
|
||||
|
||||
argsParsed[decodeURIComponent(arg).trim()] = true;
|
||||
|
||||
} else {
|
||||
|
||||
kvp = arg.split('=');
|
||||
key = decodeURIComponent(kvp[0]).trim();
|
||||
value = decodeURIComponent(kvp[1]).trim();
|
||||
argsParsed[key] = value;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return argsParsed;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.js-servername').forEach(function ($el) {
|
||||
$el.innerText = window.location.host;
|
||||
});
|
||||
|
||||
console.log(parseQuery(hash));
|
||||
console.log(parseQuery(query));
|
||||
var port = parseQuery(hash).serviceport || parseQuery(query).serviceport;
|
||||
if (port) {
|
||||
document.querySelector('.js-port').hidden = false;
|
||||
document.querySelectorAll('.js-serviceport').forEach(function ($el) {
|
||||
$el.innerText = port;
|
||||
});
|
||||
}
|
||||
|
||||
}());
|
|
@ -1,551 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var os = require('os');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports.print = function (config) {
|
||||
var services = { https: {}, http: {}, tcp: {} };
|
||||
// Note: the remote needs to know:
|
||||
// what servernames to forward
|
||||
// what ports to forward
|
||||
// what udp ports to forward
|
||||
// redirect http to https automatically
|
||||
// redirect www to nowww automatically
|
||||
if (config.http) {
|
||||
Object.keys(config.http).forEach(function (hostname) {
|
||||
if ('*' === hostname) {
|
||||
config.servernames.forEach(function (servername) {
|
||||
services.https[servername] = config.http[hostname];
|
||||
services.http[servername] = 'redirect-https';
|
||||
});
|
||||
return;
|
||||
}
|
||||
services.https[hostname] = config.http[hostname];
|
||||
services.http[hostname] = 'redirect-https';
|
||||
});
|
||||
}
|
||||
/*
|
||||
Object.keys(config.localPorts).forEach(function (port) {
|
||||
var proto = config.localPorts[port];
|
||||
if (!proto) { return; }
|
||||
if ('http' === proto) {
|
||||
config.servernames.forEach(function (servername) {
|
||||
services.http[servername] = port;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ('https' === proto) {
|
||||
config.servernames.forEach(function (servername) {
|
||||
services.https[servername] = port;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (true === proto) { proto = 'tcp'; }
|
||||
if ('tcp' !== proto) { throw new Error("unsupported protocol '" + proto + "'"); }
|
||||
//services[proxy.protocol]['*'] = proxy.port;
|
||||
//services[proxy.protocol][proxy.hostname] = proxy.port;
|
||||
services[proto]['*'] = port;
|
||||
});
|
||||
*/
|
||||
|
||||
Object.keys(services).forEach(function (protocol) {
|
||||
var subServices = services[protocol];
|
||||
Object.keys(subServices).forEach(function (hostname) {
|
||||
console.info('[local proxy]', protocol + '://' + hostname + ' => ' + subServices[hostname]);
|
||||
});
|
||||
});
|
||||
console.info('');
|
||||
};
|
||||
|
||||
module.exports.assign = function (state, tun, cb) {
|
||||
console.log('first message from', tun);
|
||||
var net = state.net || require('net');
|
||||
|
||||
function trySsh(tun, cb) {
|
||||
// https://security.stackexchange.com/questions/43231/plausibly-deniable-ssh-does-it-make-sense?rq=1
|
||||
// https://tools.ietf.org/html/rfc4253#section-4.2
|
||||
var sshPort;
|
||||
if (-1 !== ['true', 'enable', 'auto', 'on'].indexOf(state.config.sshAuto)) {
|
||||
sshPort = 22;
|
||||
} else {
|
||||
sshPort = parseInt(state.config.sshAuto, 10);
|
||||
}
|
||||
if (!sshPort || 'SSH-2.0-' !== tun.data.slice(0, 8).toString()) {
|
||||
cb(null, false);
|
||||
return;
|
||||
}
|
||||
getNetConn(sshPort, cb);
|
||||
}
|
||||
|
||||
var handlers = {};
|
||||
handlers.http = function (socket) {
|
||||
if (!state.greenlock) {
|
||||
state.greenlock = require('greenlock').create(state.greenlockConfig);
|
||||
}
|
||||
if (!state.httpRedirectServer) {
|
||||
state.redirectHttps = require('redirect-https')();
|
||||
state.httpRedirectServer = require('http').createServer(state.greenlock.middleware(state.redirectHttps));
|
||||
}
|
||||
state.httpRedirectServer.emit('connection', socket);
|
||||
process.nextTick(function () { socket.resume(); });
|
||||
};
|
||||
handlers.https = function (tlsSocket) {
|
||||
console.log('Encrypted', tlsSocket.encrypted, tlsSocket.remoteAddress, tlsSocket.remotePort);
|
||||
if (!state.defaultHttpServer) {
|
||||
state._finalHandler = require('finalhandler');
|
||||
state._serveStatic = require('serve-static');
|
||||
state._defaultServe = state._serveStatic(path.join(__dirname, 'html'));
|
||||
state.defaultHttpServer = require('http').createServer(function (req, res) {
|
||||
// TODO serve api
|
||||
state._defaultServe(req, res, state._finalHandler(req, res));
|
||||
});
|
||||
}
|
||||
state.defaultHttpServer.emit('connection', tlsSocket);
|
||||
process.nextTick(function () { tlsSocket.resume(); });
|
||||
};
|
||||
|
||||
function getNetConn(port, cb) {
|
||||
var netOpts = {
|
||||
port: port
|
||||
, host: '127.0.0.1'
|
||||
|
||||
, servername: tun.name
|
||||
, name: tun.name
|
||||
, serviceport: tun.serviceport
|
||||
, data: tun.data
|
||||
, remoteFamily: tun.family
|
||||
, remoteAddress: tun.address
|
||||
, remotePort: tun.port
|
||||
};
|
||||
var conn = net.createConnection(netOpts, function () {
|
||||
// this will happen before 'data' or 'readable' is triggered
|
||||
// We use the data from the netOpts object so that the createConnection function has
|
||||
// the oppurtunity of removing/changing it if it wants/needs to handle it differently.
|
||||
cb(null, conn);
|
||||
cb = function () {}; // for error events
|
||||
});
|
||||
conn.on('error', function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
function redirectHttp(cb) {
|
||||
var socketPair = require('socket-pair');
|
||||
var conn = socketPair.create(function (err, other) {
|
||||
if (err) { cb(err); return; }
|
||||
handlers.http(other);
|
||||
cb(null, conn);
|
||||
});
|
||||
//if (tun.data) { conn.write(tun.data); }
|
||||
return conn;
|
||||
}
|
||||
|
||||
function errorTcp(conf, cb) {
|
||||
var socketPair = require('socket-pair');
|
||||
var conn = socketPair.create(function (err, other) {
|
||||
if (err) { cb(err); return; }
|
||||
|
||||
cb(null, conn);
|
||||
|
||||
other.write("\n" +
|
||||
[ "[Telebit Error Server]"
|
||||
, "Could not load '" + conf.handler + "' as a module, file, or directory."
|
||||
].join("\n") + "\n\n");
|
||||
other.end();
|
||||
});
|
||||
//if (tun.data) { conn.write(tun.data); }
|
||||
return conn;
|
||||
}
|
||||
function fileDirTcp(conf, cb) {
|
||||
var socketPair = require('socket-pair');
|
||||
var conn = socketPair.create(function (err, other) {
|
||||
if (err) { cb(err); return; }
|
||||
|
||||
if (conf._stat.isFile()) {
|
||||
fs.createReadStream(conf.handler).pipe(other);
|
||||
} else {
|
||||
fs.readdir(conf.handler, function (err, nodes) {
|
||||
other.write('\n' + nodes.join('\n') + '\n\n');
|
||||
other.end();
|
||||
});
|
||||
}
|
||||
cb(null, conn);
|
||||
});
|
||||
//if (tun.data) { conn.write(tun.data); }
|
||||
return conn;
|
||||
}
|
||||
function echoTcp(cb) {
|
||||
var socketPair = require('socket-pair');
|
||||
var conn = socketPair.create(function (err, other) {
|
||||
if (err) { cb(err); return; }
|
||||
|
||||
other.on('data', function (chunk) {
|
||||
other.write(chunk);
|
||||
});
|
||||
|
||||
other.on('end', function () {
|
||||
other.end();
|
||||
});
|
||||
|
||||
cb(null, conn);
|
||||
|
||||
other.write("\n" +
|
||||
[ "[Telebit Echo Server] v1.0"
|
||||
, "To configure tcp run the following:"
|
||||
, "\ttelebit tcp <port number or module name>"
|
||||
, "\tex: telebit tcp 5050"
|
||||
, "\tex: telebit tcp /path/to/module"
|
||||
, "\tex: telebit tcp none"
|
||||
].join("\n") + "\n\n");
|
||||
});
|
||||
//if (tun.data) { conn.write(tun.data); }
|
||||
return conn;
|
||||
}
|
||||
|
||||
function defineProps(other, tun) {
|
||||
Object.defineProperty(other, 'remoteFamily', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return tun.family;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(other, 'remoteAddress', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return tun.address;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(other, 'remotePort', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return parseInt(tun.port);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(other, 'localPort', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return parseInt(tun.serviceport);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function invokeTcpHandler(conf, socket, tun, id, cb) {
|
||||
var conn;
|
||||
if (parseInt(conf.handler, 10)) {
|
||||
getNetConn(conf.handler, cb);
|
||||
return conn;
|
||||
}
|
||||
|
||||
var handle = tun.port;
|
||||
var handler;
|
||||
var handlerpath = conf.handler;
|
||||
var homedir = os.homedir();
|
||||
var localshare = path.join(homedir, '.local/share/telebit/apps');
|
||||
|
||||
if (/^~/.test(handlerpath)) {
|
||||
handlerpath = path.join(homedir, handlerpath.replace(/^~(\/?)/, ''));
|
||||
}
|
||||
|
||||
try {
|
||||
handler = require(handlerpath);
|
||||
console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
|
||||
} catch(e1) {
|
||||
try {
|
||||
handler = require(path.join(localshare, handlerpath));
|
||||
console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
|
||||
} catch(e2) {
|
||||
console.error("Failed to require('" + handlerpath + "'):", e1.message);
|
||||
console.error("Failed to require('" + path.join(localshare, handlerpath) + "'):", e2.message);
|
||||
console.warn("Trying static and index handlers for '" + handle + ":" + id + "'");
|
||||
handler = null;
|
||||
// fallthru
|
||||
}
|
||||
}
|
||||
|
||||
if (handler) {
|
||||
var socketPair = require('socket-pair');
|
||||
conn = socketPair.create(function (err, other) {
|
||||
handler(other, tun, id);
|
||||
cb(null, conn);
|
||||
});
|
||||
return conn;
|
||||
}
|
||||
|
||||
fs.access(conf.handler, fs.constants.R_OK, function (err1) {
|
||||
fs.stat(conf.handler, function (err2, stat) {
|
||||
if ((err1 || err2) || !(stat.isFile() || stat.isDirectory())) {
|
||||
errorTcp(conf, cb);
|
||||
return;
|
||||
}
|
||||
conf._stat = stat;
|
||||
fileDirTcp(conf, cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
var handlerservers = {};
|
||||
function invokeHandler(conf, tlsSocket, tun, id) {
|
||||
if (parseInt(conf.handler, 10)) {
|
||||
// TODO http-proxy with proper headers and ws support
|
||||
getNetConn(conf.handler, function (err, conn) {
|
||||
process.nextTick(function () { tlsSocket.resume(); });
|
||||
if (err) {
|
||||
require('./handlers/local-app-error.js')({ handler: conf.handler, socket: tlsSocket });
|
||||
return;
|
||||
}
|
||||
console.info("Port-Forwarding '" + (tun.name || tun.serviceport) + "' to '" + conf.handler + "'");
|
||||
conn.pipe(tlsSocket);
|
||||
tlsSocket.pipe(conn);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var handle = tun.name || tun.port;
|
||||
var handler;
|
||||
var handlerpath = conf.handler;
|
||||
var homedir = os.homedir();
|
||||
var localshare = path.join(homedir, '.local/share/telebit/apps');
|
||||
var http = require('http');
|
||||
|
||||
// 1. No modification handlerpath may be an aboslute path
|
||||
// 2. it may be relative to a user home directory
|
||||
// 3. it may be relative to a user ~/local/share
|
||||
|
||||
tlsSocket._tun = tun;
|
||||
tlsSocket._id = id;
|
||||
if (handlerservers[conf.handler]) {
|
||||
handlerservers[conf.handler].emit('connection', tlsSocket);
|
||||
process.nextTick(function () { tlsSocket.resume(); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^~/.test(handlerpath)) {
|
||||
// TODO have the telebit remote tell which user is running
|
||||
handlerpath = path.join(homedir, handlerpath.replace(/^~(\/?)/, ''));
|
||||
}
|
||||
|
||||
try {
|
||||
handler = require(handlerpath);
|
||||
console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
|
||||
} catch(e1) {
|
||||
try {
|
||||
handler = require(path.join(localshare, handlerpath));
|
||||
console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
|
||||
} catch(e2) {
|
||||
console.error("Failed to require('" + handlerpath + "'):", e1.message);
|
||||
console.error("Failed to require('" + path.join(localshare, handlerpath) + "'):", e2.message);
|
||||
console.warn("Trying static and index handlers for '" + handle + ":" + id + "'");
|
||||
handler = null;
|
||||
// fallthru
|
||||
}
|
||||
}
|
||||
|
||||
if (handler) {
|
||||
handlerservers[conf.handler] = http.createServer(handler);
|
||||
handlerservers[conf.handler].emit('connection', tlsSocket);
|
||||
process.nextTick(function () { tlsSocket.resume(); });
|
||||
return;
|
||||
}
|
||||
|
||||
fs.access(conf.handler, fs.constants.R_OK, function (err1) {
|
||||
fs.stat(conf.handler, function (err2, stat) {
|
||||
if (err1 || err2) {
|
||||
// TODO handle errors
|
||||
handlers.https(tlsSocket, tun, id);
|
||||
return;
|
||||
}
|
||||
var isFile = stat.isFile();
|
||||
state._finalHandler = require('finalhandler');
|
||||
state._serveStatic = require('serve-static');
|
||||
state._serveIndex = require('serve-index');
|
||||
var serveIndex;
|
||||
var serveStatic;
|
||||
if (isFile) {
|
||||
serveStatic = state._serveStatic(path.dirname(conf.handler), { dotfiles: 'allow', index: [ 'index.html' ] });
|
||||
serveIndex = function (req, res, next) { next(); };
|
||||
isFile = path.basename(conf.handler);
|
||||
} else {
|
||||
serveStatic = state._serveStatic(conf.handler, { dotfiles: 'allow', index: [ 'index.html' ] });
|
||||
serveIndex = state._serveIndex(conf.handler, { hidden: true, icons: true, view: 'tiles' });
|
||||
}
|
||||
handler = function (req, res) {
|
||||
if (isFile) {
|
||||
req.url = '/' + isFile;
|
||||
}
|
||||
serveStatic(req, res, function () {
|
||||
serveIndex(req, res, state._finalHandler(req, res));
|
||||
});
|
||||
};
|
||||
handlerservers[conf.handler] = http.createServer(handler);
|
||||
handlerservers[conf.handler].emit('connection', tlsSocket);
|
||||
process.nextTick(function () { tlsSocket.resume(); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function terminateTls(tun, cb) {
|
||||
var socketPair = require('socket-pair');
|
||||
var conn = socketPair.create(function (err, other) {
|
||||
if (err) { cb(err); return; }
|
||||
|
||||
//console.log('[hit tcp connection]', other.remoteFamily, other.remoteAddress, other.remotePort, other.localPort);
|
||||
defineProps(other, tun);
|
||||
//console.log('[hit tcp connection]', other.remoteFamily, other.remoteAddress, other.remotePort, other.localPort);
|
||||
|
||||
if (!state.greenlock) {
|
||||
state.greenlock = require('greenlock').create(state.greenlockConfig);
|
||||
}
|
||||
|
||||
if (!state.terminatorServer) {
|
||||
state.terminatorServer = require('tls').createServer(state.greenlock.tlsOptions, function (tlsSocket) {
|
||||
var Packer = require('proxy-packer');
|
||||
var addr = Packer.socketToAddr(tlsSocket);
|
||||
var id = Packer.addrToId(addr);
|
||||
|
||||
defineProps(tlsSocket, addr);
|
||||
//console.log('[hit tls server]', tlsSocket.remoteFamily, tlsSocket.remoteAddress, tlsSocket.remotePort, tlsSocket.localPort);
|
||||
//console.log(addr);
|
||||
var conf = state.servernames[tlsSocket.servername];
|
||||
tlsSocket.once('data', function (firstChunk) {
|
||||
tlsSocket.pause();
|
||||
//tlsSocket.unshift(firstChunk);
|
||||
tlsSocket._handle.onread(firstChunk.length, firstChunk);
|
||||
|
||||
trySsh({ data: firstChunk }, function (err, conn) {
|
||||
if (conn) {
|
||||
conn.pipe(tlsSocket);
|
||||
tlsSocket.pipe(conn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conf || !conf.handler || 'none' === conf.handler) {
|
||||
console.log('https default handler');
|
||||
handlers.https(tlsSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('https invokeHandler');
|
||||
invokeHandler(conf, tlsSocket, tun, id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//console.log('[hit tcp connection]', other.remoteFamily, other.remoteAddress, other.remotePort, other.localPort);
|
||||
state.terminatorServer.emit('connection', other);
|
||||
cb(null, conn);
|
||||
});
|
||||
//if (tun.data) { conn.write(tun.data); }
|
||||
return conn;
|
||||
}
|
||||
|
||||
var handled;
|
||||
|
||||
if (!tun.name && !tun.serviceport) {
|
||||
console.log('tun:\n',tun);
|
||||
//console.warn(tun.data.toString());
|
||||
cb(new Error("No routing information for ':tun_id'. Missing both 'name' and 'serviceport'."));
|
||||
return;
|
||||
}
|
||||
|
||||
if ('http' === tun.service || 'https' === tun.service) {
|
||||
if (!tun.name) {
|
||||
cb(new Error("No routing information for ':tun_id'. Service '" + tun.service + "' is missing 'name'."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ('http' === tun.service) {
|
||||
// TODO match *.example.com
|
||||
handled = Object.keys(state.servernames).some(function (sn) {
|
||||
if (sn !== tun.name) { return; }
|
||||
|
||||
console.log('Found config match for PLAIN', tun.name);
|
||||
if (!state.servernames[sn]) { return; }
|
||||
|
||||
if (false === state.servernames[sn].terminate) {
|
||||
cb(new Error("insecure http not supported yet"));
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Redirecting HTPTP for', tun.name);
|
||||
redirectHttp(cb);
|
||||
return true;
|
||||
});
|
||||
if (!handled) {
|
||||
redirectHttp(cb);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ('https' === tun.service) {
|
||||
// TODO match *.example.com
|
||||
handled = Object.keys(state.servernames).some(function (sn) {
|
||||
if (sn !== tun.name) { return; }
|
||||
|
||||
console.log('Found config match for TLS', tun.name);
|
||||
if (!state.servernames[sn]) { return; }
|
||||
|
||||
if (false === state.servernames[sn].terminate) {
|
||||
cb(new Error("insecure http not supported yet"));
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Terminating TLS for', tun.name);
|
||||
terminateTls(tun, cb);
|
||||
return true;
|
||||
});
|
||||
if (!handled) {
|
||||
terminateTls(tun, cb);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ('tcp' === tun.service) {
|
||||
trySsh(tun, function (err, conn) {
|
||||
if (conn) { cb(null, conn); return; }
|
||||
// TODO add TCP handlers
|
||||
var conf = state.ports[tun.serviceport];
|
||||
if (!conf || !conf.handler || 'none' === conf.handler) {
|
||||
console.log('Using echo server for tcp');
|
||||
echoTcp(cb);
|
||||
return;
|
||||
}
|
||||
|
||||
var Packer = require('proxy-packer');
|
||||
//var addr = Packer.socketToAddr(conn);
|
||||
var id = Packer.addrToId(tun);
|
||||
invokeTcpHandler(conf, conn, tun, id, cb);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn("Unknown service '" + tun.service + "'");
|
||||
|
||||
/*
|
||||
var portList = state.services[service];
|
||||
var port;
|
||||
port = portList[tun.name];
|
||||
if (!port) {
|
||||
// Check for any wildcard domains, sorted longest to shortest so the one with the
|
||||
// biggest natural match will be found first.
|
||||
Object.keys(portList).filter(function (pattern) {
|
||||
return pattern[0] === '*' && pattern.length > 1;
|
||||
}).sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
}).some(function (pattern) {
|
||||
var subPiece = pattern.slice(1);
|
||||
if (subPiece === tun.name.slice(-subPiece.length)) {
|
||||
port = portList[pattern];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!port) {
|
||||
port = portList['*'];
|
||||
}
|
||||
*/
|
||||
};
|
140
lib/updater.js
140
lib/updater.js
|
@ -1,140 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function (pkg) {
|
||||
function checkUpgrade() {
|
||||
var https = require('https');
|
||||
|
||||
function getFile(url, cb) {
|
||||
https.get(url, function (resp) {
|
||||
var str = '';
|
||||
resp.on('data', function (chunk) {
|
||||
str += chunk.toString('utf8');
|
||||
});
|
||||
resp.on('end', function () {
|
||||
cb(null, str);
|
||||
});
|
||||
resp.on('error', function (err) {
|
||||
// ignore
|
||||
cb(err);
|
||||
});
|
||||
}).on('error', function (err) {
|
||||
// ignore
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
function isNewer(latest, myPkg) {
|
||||
//console.log('sort result:', sortLatest(latest, myPkg));
|
||||
return sortLatest(latest, myPkg) < 0;
|
||||
}
|
||||
function sortLatest(latest, myPkg) {
|
||||
var m = /^(v)?(\d+)\.(\d+)\.(\d+)(.*)/.exec(latest);
|
||||
var n = /^(v)?(\d+)\.(\d+)\.(\d+)(.*)/.exec(myPkg);
|
||||
//console.log('m', m);
|
||||
//console.log('n', n);
|
||||
if (!m) {
|
||||
if (!n) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
} else if (!n) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (parseInt(m[2], 10) > parseInt(n[2], 10)) {
|
||||
return -1;
|
||||
} else if (parseInt(m[2], 10) === parseInt(n[2], 10)) {
|
||||
if (parseInt(m[3], 10) > parseInt(n[3], 10)) {
|
||||
return -1;
|
||||
} else if (parseInt(m[3], 10) === parseInt(n[3], 10)) {
|
||||
if (parseInt(m[4], 10) > parseInt(n[4], 10)) {
|
||||
return -1;
|
||||
} else if (parseInt(m[4], 10) === parseInt(n[4], 10)) {
|
||||
// lex sorting
|
||||
if (m[5] > n[5]) {
|
||||
return -1;
|
||||
} else if (m[5] === n[5]) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
getFile("https://telebit.cloud/dist/index.tab", function (err, tab) {
|
||||
if (err) { /*ignore*/ return; }
|
||||
if (tab) { tab = tab && tab.toString() || ''; }
|
||||
var versions = [];
|
||||
var lines = tab.split(/[\r\n]/g);
|
||||
var headers = lines.shift().split(/\t/g);
|
||||
var chan = 'prod';
|
||||
var next;
|
||||
lines.forEach(function (line) {
|
||||
var tsv = {};
|
||||
var fields = line.split(/\t/g);
|
||||
fields.forEach(function (value, i) {
|
||||
tsv[headers[i]] = value;
|
||||
});
|
||||
versions.push(tsv);
|
||||
});
|
||||
// find matching version
|
||||
versions.some(function (v) {
|
||||
if (('v' + pkg.version) === v.version) {
|
||||
chan = v.channel;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// find first (most recent) version in channel
|
||||
versions.some(function (v) {
|
||||
if (chan === v.channel) {
|
||||
next = v;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!next || !isNewer(next.version, pkg.version)) {
|
||||
//console.log('DEBUG can\'t upgrade from', pkg.version, 'in channel', chan);
|
||||
return;
|
||||
}
|
||||
console.log('Upgrade Available: ' + next.version + ' in \'' + next.channel + '\'channel');
|
||||
getFile("https://telebit.cloud/dist/upgrade.js", function (err, script) {
|
||||
if (err) { /*ignore*/ return; }
|
||||
var os = require('os');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var scriptname = 'telebit-upgrade-' + Math.round(Math.random() * 99999) + '.js';
|
||||
var pathname = path.join(os.tmpdir(), scriptname);
|
||||
fs.writeFile(pathname, script, function (err) {
|
||||
if (err) { /*ignore*/ return; }
|
||||
// console.log('DEBUG wrote', pathname);
|
||||
//var str =
|
||||
require(pathname)({
|
||||
package: pkg
|
||||
, root: path.resolve(__dirname, '..')
|
||||
, latest: next
|
||||
, channel: chan
|
||||
}, function () {
|
||||
// console.log('upgrade complete');
|
||||
});
|
||||
//console.log(str);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var _interval = setInterval(checkUpgrade, 2 * 60 * 60 * 1000);
|
||||
process.nextTick(function () {
|
||||
checkUpgrade();
|
||||
});
|
||||
|
||||
return function cancel() {
|
||||
clearInterval(_interval);
|
||||
};
|
||||
};
|
49
package.json
49
package.json
|
@ -1,23 +1,19 @@
|
|||
{
|
||||
"name": "telebit",
|
||||
"version": "0.19.26",
|
||||
"description": "Break out of localhost. Connect to any device from anywhere over any tcp port or securely in a browser. A secure tunnel. A poor man's reverse VPN.",
|
||||
"main": "lib/remote.js",
|
||||
"files": [
|
||||
"bin",
|
||||
"lib",
|
||||
"usr"
|
||||
],
|
||||
"name": "stunnel",
|
||||
"version": "0.10.4",
|
||||
"description": "A pure-JavaScript tunnel client for http and https similar to localtunnel.me, but uses TLS (SSL) with ServerName Indication (SNI) over https to work even in harsh network conditions such as in student dorms and behind HOAs, corporate firewalls, public libraries, airports, airplanes, etc. Can also tunnel tls and plain tcp.",
|
||||
"main": "wsclient.js",
|
||||
"bin": {
|
||||
"telebit": "bin/telebit.js",
|
||||
"telebitd": "bin/telebitd.js"
|
||||
"jstunnel": "bin/stunnel.js",
|
||||
"stunnel.js": "bin/stunnel.js",
|
||||
"stunnel-js": "bin/stunnel.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.coolaj86.com/coolaj86/telebit.js.git"
|
||||
"url": "git+ssh://git@git.coolaj86.com/coolaj86/tunnel-client.js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
@ -36,9 +32,6 @@
|
|||
"tunnel",
|
||||
"localtunnel",
|
||||
"localtunnel.me",
|
||||
"underpass",
|
||||
"ngrok",
|
||||
"ngrok.io",
|
||||
"proxy",
|
||||
"reverse",
|
||||
"reverse-proxy",
|
||||
|
@ -49,30 +42,16 @@
|
|||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bugs": {
|
||||
"url": "https://git.coolaj86.com/coolaj86/telebit.js/issues"
|
||||
"url": "https://git.coolaj86.com/coolaj86/tunnel-client.js/issues"
|
||||
},
|
||||
"homepage": "https://git.coolaj86.com/coolaj86/telebit.js#readme",
|
||||
"homepage": "https://git.coolaj86.com/coolaj86/tunnel-client.js#readme",
|
||||
"dependencies": {
|
||||
"@coolaj86/urequest": "^1.3.5",
|
||||
"finalhandler": "^1.1.1",
|
||||
"greenlock": "^2.3.1",
|
||||
"js-yaml": "^3.11.0",
|
||||
"bluebird": "^3.5.0",
|
||||
"commander": "^2.9.0",
|
||||
"jsonwebtoken": "^7.1.9",
|
||||
"mkdirp": "^0.5.1",
|
||||
"proxy-packer": "^1.4.3",
|
||||
"recase": "^1.0.4",
|
||||
"redirect-https": "^1.1.5",
|
||||
"serve-index": "^1.9.1",
|
||||
"serve-static": "^1.13.2",
|
||||
"oauth3.js": "git+https://git.oauth3.org/OAuth3/oauth3.js.git#v1",
|
||||
"sni": "^1.0.0",
|
||||
"socket-pair": "^1.0.3",
|
||||
"tunnel-packer": "^1.2.0",
|
||||
"ws": "^2.2.3"
|
||||
},
|
||||
"trulyOptionalDependencies": {
|
||||
"bluebird": "^3.5.1"
|
||||
},
|
||||
"enginesStrict": true,
|
||||
"engines": {
|
||||
"node": "10.2.1 10.4 10.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
var WebSocket = require('ws');
|
||||
var jwt = require('jsonwebtoken');
|
||||
var hostname = 'example.daplie.me';
|
||||
var token = jwt.sign({ name: hostname }, 'shhhhh');
|
||||
var url = 'wss://stunnel.hellabit.com:3000/?access_token=' + token;
|
||||
var wstunneler = new WebSocket(url, { rejectUnauthorized: false });
|
||||
|
||||
wstunneler.on('open', function () {
|
||||
console.log('open');
|
||||
});
|
||||
|
||||
wstunneler.on('error', function (err) {
|
||||
console.error(err.toString());
|
||||
});
|
||||
|
||||
}());
|
|
@ -1,21 +0,0 @@
|
|||
There are a number of conditions and whatnot that must be tested in more-or-less real-world conditions.
|
||||
|
||||
telebit init // fresh install
|
||||
telebit init // after install complete
|
||||
|
||||
telebit http 3000 // have an app listening on localhost:3000
|
||||
telebit http 4545 // do not have an app listening
|
||||
|
||||
telebit http ./path/to/site
|
||||
telebit http ./path/to/dir
|
||||
telebit http ./path/to/file
|
||||
telebit http ./doesnt/exist
|
||||
|
||||
telebit ssh auto // do have ssh listening on localhost:22
|
||||
telebit ssh 4545 // do have ssh listenening
|
||||
|
||||
telebit tcp 3000 // have an echo server listening on localhost:3000
|
||||
telebit tcp 4545 // no server listening
|
||||
|
||||
telebit tcp ./path/to/file
|
||||
telebit tcp ./path/to/dir
|
|
@ -1,27 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var net = require('net');
|
||||
var server = net.createServer(function (conn) {
|
||||
function echo(chunk) {
|
||||
conn.write(chunk);
|
||||
if (chunk.length <= 10 && /\b(q|quit|end|cancel)\b/i.test(chunk.toString('utf8'))) {
|
||||
conn.end();
|
||||
conn.removeListener('data', echo);
|
||||
}
|
||||
}
|
||||
conn.on('data', echo);
|
||||
// NOTE: early versions of telebit do not support a 'connection' event
|
||||
// and therefore will say hello after the first message from the client
|
||||
conn.write(
|
||||
"[Echo Server] Hello! I'm an echo server.\n"
|
||||
+ "[Echo Server] I try to be your friend but when I see things like q|quit|end|cancel, I give up.\n"
|
||||
);
|
||||
});
|
||||
server.on('error', function (err) {
|
||||
console.error("[echo server]");
|
||||
console.error(err);
|
||||
});
|
||||
server.listen(process.argv[2] || 3000, function () {
|
||||
console.info("Listening on", this.address());
|
||||
console.info('ctrl+c to cancel');
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var email = 'jon@example.com';
|
||||
var pin = Math.round(Math.random() * 999999).toString().padStart(6, '0'); // '321654'
|
||||
|
||||
console.log('Pair Code:', pin);
|
||||
|
||||
var urequest = require('@coolaj86/urequest');
|
||||
var req = {
|
||||
url: 'https://api.telebit.ppl.family/api/telebit.cloud/pair_request'
|
||||
, method: 'POST'
|
||||
, headers: { 'cOntEnt-tYpE': 'application/json;charset=utf-8' }
|
||||
, json: {
|
||||
subject: email
|
||||
, subject_scheme: 'mailto'
|
||||
, scope: ''
|
||||
, otp: pin
|
||||
, hostname: "User's Macbook Pro"
|
||||
, os_type: 'Linux'
|
||||
, os_platform: 'linux'
|
||||
, os_release: '4.4.0-116-generic'
|
||||
, os_arch: 'x64'
|
||||
}
|
||||
};
|
||||
urequest(req, function (err, resp, body) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.log('Location:', resp.headers.location);
|
||||
console.log('Body:');
|
||||
console.log(body);
|
||||
/*
|
||||
{ jwt: '...'
|
||||
}
|
||||
*/
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var stateUrl = 'https://api.telebit.ppl.family/api/telebit.cloud/pair_state/bca27428719e9c67805359f1';
|
||||
|
||||
var urequest = require('@coolaj86/urequest');
|
||||
var req = {
|
||||
url: stateUrl
|
||||
, method: 'GET'
|
||||
, json: true
|
||||
};
|
||||
urequest(req, function (err, resp, body) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.log('Done:');
|
||||
console.log(body);
|
||||
/*
|
||||
body.status = 'ready' | 'pending' | 'complete' | 'invalid'
|
||||
body.access_token // only in 'ready' state
|
||||
*/
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var os = require('os');
|
||||
var net = require('net');
|
||||
var ipc = {
|
||||
path: /^win/.test(os.platform()) ? '\\\\.\\pipe\\X:/name/of/pipe' : (__dirname + '/tmp.sock')
|
||||
};
|
||||
var oldUmask = process.umask(0x0000);
|
||||
var server = net.createServer();
|
||||
|
||||
server.listen({
|
||||
path: ipc.path || null
|
||||
, host: 'localhost'
|
||||
, port: ipc.port || null
|
||||
, writeableAll: true
|
||||
, readableAll: true
|
||||
}, function () {
|
||||
process.umask(oldUmask);
|
||||
console.log("Listening on", this.address());
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
var spawn = require('child_process').spawn;
|
||||
var args = [
|
||||
path.join(__dirname, 'windows-pipe.js')
|
||||
];
|
||||
var subprocess = spawn(
|
||||
'node'
|
||||
, args
|
||||
, { detached: true
|
||||
, stdio: [ 'ignore', process.stdout, process.stderr ]
|
||||
}
|
||||
);
|
||||
//console.log('[debug]', vars.telebitNode, args.join(' '));
|
||||
subprocess.unref();
|
||||
subprocess.on('error', function (_err) {
|
||||
console.error(_err);
|
||||
});
|
||||
subprocess.on('exit', function (code, signal) {
|
||||
console.error('' + code + ' ' + signal + ' failure to launch');
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>Telebit Remote</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{TELEBIT_NODE}</string>
|
||||
<string>{TELEBITD_JS}</string>
|
||||
<string>daemon</string>
|
||||
<string>--config</string>
|
||||
<string>{TELEBITD_CONFIG}</string>
|
||||
</array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>TELEBIT_PATH</key>
|
||||
<string>{TELEBIT_PATH}</string>
|
||||
<key>NODE_PATH</key>
|
||||
<string>{NODE_PATH}</string>
|
||||
<key>NPM_CONFIG_PREFIX</key>
|
||||
<string>{NPM_CONFIG_PREFIX}</string>
|
||||
</dict>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>{TELEBIT_USER}</string>
|
||||
<key>GroupName</key>
|
||||
<string>{TELEBIT_GROUP}</string>
|
||||
<key>InitGroups</key>
|
||||
<true/>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<!--dict>
|
||||
<key>Crashed</key>
|
||||
<true/>
|
||||
<key>NetworkState</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>{TELEBIT_PATH}</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{TELEBIT_LOG_DIR}/error.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{TELEBIT_LOG_DIR}/info.log</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/bash
|
||||
{TELEBIT_NODE} {TELEBIT_JS} "$@"
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/bash
|
||||
{TELEBIT_NODE} {TELEBITD_JS} daemon "$@"
|
|
@ -1,64 +0,0 @@
|
|||
# Pre-req
|
||||
# sudo adduser telebit --home {TELEBIT_PATH}
|
||||
# sudo mkdir -p {TELEBIT_PATH}/
|
||||
# sudo chown -R {TELEBIT_USER}:{TELEBIT_GROUP} {TELEBIT_PATH}/
|
||||
|
||||
[Unit]
|
||||
Description=Telebit Remote
|
||||
Documentation=https://git.coolaj86.com/coolaj86/telebit.js/
|
||||
; After=network-online.target
|
||||
; Wants=network-online.target systemd-networkd-wait-online.service
|
||||
|
||||
[Service]
|
||||
# Restart on crash (bad signal), and also 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=always
|
||||
StartLimitInterval=10
|
||||
StartLimitBurst=3
|
||||
|
||||
# User and group the process will run as
|
||||
;User={TELEBIT_USER}
|
||||
;Group={TELEBIT_GROUP}
|
||||
|
||||
WorkingDirectory={TELEBIT_PATH}
|
||||
# custom directory cannot be set and will be the place where this exists, not the working directory
|
||||
ExecStart={TELEBIT_NODE} {TELEBITD_JS} daemon --config {TELEBITD_CONFIG}
|
||||
ExecReload=/bin/kill -USR1 $MAINPID
|
||||
|
||||
# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
|
||||
# Unmodified, this is not expected to use more than this.
|
||||
;LimitNOFILE=1048576 # no issues yet, but disabled just in case
|
||||
;LimitNPROC=64 # doesn't work on some systems
|
||||
|
||||
# Use private /tmp and /var/tmp, which are discarded after this 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 for a few because we want a place for config, logs, etc
|
||||
# This merely retains r/w access rights, it does not add any new.
|
||||
# Must still be writable on the host!
|
||||
ReadWriteDirectories={TELEBIT_RW_DIRS}
|
||||
|
||||
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
|
||||
; ReadWritePaths={TELEBIT_RW_DIRS}
|
||||
|
||||
# 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 features may need additional capabilities.
|
||||
# For example an "upload" may need CAP_LEASE
|
||||
; CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||
; AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||
; NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,70 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>Telebit Remote</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{TELEBIT_NODE}</string>
|
||||
<string>{TELEBITD_JS}</string>
|
||||
<string>daemon</string>
|
||||
<string>--config</string>
|
||||
<string>{TELEBITD_CONFIG}</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>TELEBIT_PATH</key>
|
||||
<string>{TELEBIT_PATH}</string>
|
||||
<key>NODE_PATH</key>
|
||||
<string>{NODE_PATH}</string>
|
||||
<key>NPM_CONFIG_PREFIX</key>
|
||||
<string>{NPM_CONFIG_PREFIX}</string>
|
||||
</dict>
|
||||
|
||||
<!--
|
||||
:: LaunchDaemon Only ::
|
||||
<key>UserName</key>
|
||||
<string>{TELEBIT_USER}</string>
|
||||
<key>GroupName</key>
|
||||
<string>{TELEBIT_GROUP}</string>
|
||||
<key>InitGroups</key>
|
||||
<true/>
|
||||
-->
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<!--
|
||||
<dict>
|
||||
<key>Crashed</key>
|
||||
<true/>
|
||||
<key>NetworkState</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>{TELEBIT_PATH}</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{TELEBIT_LOG_DIR}/error.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{TELEBIT_LOG_DIR}/info.log</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,64 +0,0 @@
|
|||
# Pre-req
|
||||
# sudo adduser telebit --home {TELEBIT_PATH}
|
||||
# sudo mkdir -p {TELEBIT_PATH}/
|
||||
# sudo chown -R {TELEBIT_USER}:{TELEBIT_GROUP} {TELEBIT_PATH}/
|
||||
|
||||
[Unit]
|
||||
Description=Telebit Remote
|
||||
Documentation=https://git.coolaj86.com/coolaj86/telebit.js/
|
||||
After=network-online.target
|
||||
Wants=network-online.target systemd-networkd-wait-online.service
|
||||
|
||||
[Service]
|
||||
# Restart on crash (bad signal), and also 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=always
|
||||
StartLimitInterval=10
|
||||
StartLimitBurst=3
|
||||
|
||||
# User and group the process will run as
|
||||
User={TELEBIT_USER}
|
||||
Group={TELEBIT_GROUP}
|
||||
|
||||
WorkingDirectory={TELEBIT_PATH}
|
||||
# custom directory cannot be set and will be the place where this exists, not the working directory
|
||||
ExecStart={TELEBIT_NODE} {TELEBITD_JS} daemon --config {TELEBITD_CONFIG}
|
||||
ExecReload=/bin/kill -USR1 $MAINPID
|
||||
|
||||
# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
|
||||
# Unmodified, this is not expected to use more than this.
|
||||
LimitNOFILE=1048576
|
||||
LimitNPROC=64
|
||||
|
||||
# Use private /tmp and /var/tmp, which are discarded after this 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 for a few because we want a place for config, logs, etc
|
||||
# This merely retains r/w access rights, it does not add any new.
|
||||
# Must still be writable on the host!
|
||||
ReadWriteDirectories={TELEBIT_RW_DIRS}
|
||||
|
||||
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
|
||||
; ReadWritePaths={TELEBIT_RW_DIRS}
|
||||
|
||||
# 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 features may need additional capabilities.
|
||||
# For example an "upload" may need CAP_LEASE
|
||||
; CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||
; AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||
; NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,306 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
//var fs = require('fs');
|
||||
var os = require('os');
|
||||
var mkdirp = require('mkdirp');
|
||||
var exec = require('child_process').exec;
|
||||
var path = require('path');
|
||||
|
||||
var Launcher = module.exports;
|
||||
Launcher.install = function (things, fn) {
|
||||
if (!fn) { fn = function (err) { if (err) { console.error(err); } }; }
|
||||
things = things || {};
|
||||
// in some future version we can take this file out
|
||||
// and accept process.env from things
|
||||
var installLauncher = require('./template-launcher');
|
||||
|
||||
// Right now this is just for npm install -g and npx
|
||||
if (things.env) {
|
||||
things.env.PATH = things.env.PATH || process.env.PATH;
|
||||
} else {
|
||||
things.env = process.env;
|
||||
}
|
||||
things.argv = things.argv || process.argv;
|
||||
things._execOpts = { windowsHide: true, env: things.env };
|
||||
var telebitRoot = path.join(__dirname, '../..');
|
||||
var vars = {
|
||||
telebitPath: telebitRoot
|
||||
, telebitUser: os.userInfo().username
|
||||
, telebitGroup: (/^darwin/i.test(os.platform()) ? 'staff' : os.userInfo().username)
|
||||
, telebitRwDirs: [
|
||||
telebitRoot
|
||||
, path.join(os.homedir(), '.config/telebit')
|
||||
, path.join(os.homedir(), '.local/share/telebit')
|
||||
]
|
||||
, telebitNode: (things.argv[0]||'').replace(/\.exe/i, '') // path.join(telebitRoot, 'bin/node')
|
||||
, telebitBin: path.join(telebitRoot, 'bin/telebit')
|
||||
, telebitdBin: path.join(telebitRoot, 'bin/telebitd')
|
||||
, telebitJs: path.join(telebitRoot, 'bin/telebit.js')
|
||||
, telebitdJs: path.join(telebitRoot, 'bin/telebitd.js')
|
||||
, telebitConfig: path.join(os.homedir(), '.config/telebit/telebit.yml')
|
||||
, telebitdConfig: path.join(os.homedir(), '.config/telebit/telebitd.yml')
|
||||
, TELEBIT_LOG_DIR: path.join(os.homedir(), '.local/share/telebit/var/log')
|
||||
, TELEBIT_SOCK_DIR: path.join(os.homedir(), '.local/share/telebit/var/run')
|
||||
};
|
||||
vars.telebitBinTpl = path.join(telebitRoot, 'usr/share/dist/bin/telebit.tpl');
|
||||
vars.telebitNpm = path.resolve(vars.telebitNode, '../npm');
|
||||
vars.nodePath = path.resolve(vars.telebitNode, '../lib/node_modules');
|
||||
vars.npmConfigPrefix = path.resolve(vars.telebitNode, '..');
|
||||
vars.userspace = (!things.telebitUser || (things.telebitUser === os.userInfo().username)) ? true : false;
|
||||
if (-1 === vars.telebitRwDirs.indexOf(vars.npmConfigPrefix)) {
|
||||
vars.telebitRwDirs.push(vars.npmConfigPrefix);
|
||||
}
|
||||
vars.telebitRwDirs = vars.telebitRwDirs.join(' ');
|
||||
function getError(err, stderr) {
|
||||
if (err) { return err; }
|
||||
if (stderr) {
|
||||
err = new Error(stderr);
|
||||
err.code = 'ELAUNCHER';
|
||||
return err;
|
||||
}
|
||||
}
|
||||
var launchers = {
|
||||
'node': function () {
|
||||
var fs = require('fs');
|
||||
var spawn = require('child_process').spawn;
|
||||
var logpath = path.join(os.homedir(), '.local/share/telebit/var/log');
|
||||
try {
|
||||
mkdirp.sync(logpath);
|
||||
} catch(e) {
|
||||
if (fn) { fn(e); return; }
|
||||
return;
|
||||
}
|
||||
var stdout = fs.openSync(path.join(logpath, 'info.log'), 'a');
|
||||
var stderr = fs.openSync(path.join(logpath, 'error.log'), 'a');
|
||||
|
||||
var killed = 0;
|
||||
var err;
|
||||
var args = [
|
||||
path.join(telebitRoot, 'bin/telebitd.js')
|
||||
, 'daemon'
|
||||
, '--config'
|
||||
, vars.telebitdConfig
|
||||
];
|
||||
var subprocess = spawn(
|
||||
vars.telebitNode
|
||||
, args
|
||||
, { detached: true
|
||||
, stdio: [ 'ignore', stdout, stderr ]
|
||||
}
|
||||
);
|
||||
//console.log('[debug]', vars.telebitNode, args.join(' '));
|
||||
subprocess.unref();
|
||||
subprocess.on('error', function (_err) {
|
||||
err = _err;
|
||||
killed += 1;
|
||||
});
|
||||
subprocess.on('exit', function (code, signal) {
|
||||
if (!err) { err = new Error('' + code + ' ' + signal + ' failure to launch'); }
|
||||
killed += 1;
|
||||
});
|
||||
|
||||
// Two things:
|
||||
// 1) wait to see if the process dies
|
||||
// 2) wait to give time for the socket to connect
|
||||
setTimeout(function () {
|
||||
if (fn) { fn(err); return; }
|
||||
}, 1.75 * 1000);
|
||||
return;
|
||||
}
|
||||
, 'launchctl': function () {
|
||||
var launcher = path.join(os.homedir(), 'Library/LaunchAgents/cloud.telebit.remote.plist');
|
||||
try {
|
||||
mkdirp.sync(path.join(os.homedir(), 'Library/LaunchAgents'));
|
||||
installLauncher.sync({
|
||||
file: {
|
||||
tpl: vars.telebitBinTpl
|
||||
, launcher: path.join(vars.telebitPath, 'bin/telebit')
|
||||
, executable: true
|
||||
}
|
||||
, vars: vars
|
||||
});
|
||||
installLauncher({
|
||||
file: {
|
||||
tpl: path.join(vars.telebitPath, 'usr/share/dist/etc/skel/Library/LaunchAgents/cloud.telebit.remote.plist.tpl')
|
||||
, launcher: launcher
|
||||
}
|
||||
, vars: vars
|
||||
});
|
||||
var launcherstr = (vars.userspace ? "" : "sudo ") + "launchctl ";
|
||||
var execstr = launcherstr + "unload -w " + launcher;
|
||||
exec(execstr, things._execOpts, function (/*err, stdout, stderr*/) {
|
||||
// we probably only need to skip the stderr (saying that it can't stop something that isn't started)
|
||||
//err = getError(err, stderr);
|
||||
//if (err) { fn(err); return; }
|
||||
//console.log((stdout||'').trim());
|
||||
//console.log('unload worked?');
|
||||
execstr = launcherstr + "load -w " + launcher;
|
||||
exec(execstr, things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, stderr);
|
||||
if (err) { fn(err); return; }
|
||||
//console.log((stdout||'').trim());
|
||||
//console.log('load worked?');
|
||||
setTimeout(function () {
|
||||
fn(null);
|
||||
}, 1.25 * 1000);
|
||||
});
|
||||
});
|
||||
} catch(e) {
|
||||
console.error("'" + launcher + "' error:");
|
||||
console.error(e);
|
||||
if (fn) { fn(e); return; }
|
||||
}
|
||||
}
|
||||
, 'systemctl': function () {
|
||||
var launcher = path.join(os.homedir(), '.config/systemd/user/telebit.service');
|
||||
var launchername = 'telebit.service';
|
||||
try {
|
||||
mkdirp.sync(path.join(os.homedir(), '.config/systemd/user'));
|
||||
installLauncher({
|
||||
file: {
|
||||
tpl: path.join(vars.telebitPath, 'usr/share/dist/etc/skel/.config/systemd/user/telebit.service.tpl')
|
||||
, launcher: launcher
|
||||
}
|
||||
, vars: vars
|
||||
}, function () {
|
||||
// IMPORTANT
|
||||
// It's a dangerous to go alone, take this:
|
||||
// SYSTEMD_LOG_LEVEL=debug journalctl -xef --user-unit=telebit
|
||||
// (makes debugging systemd issues not "easy" per se, but possible)
|
||||
var launcherstr = (vars.userspace ? "" : "sudo ") + "systemctl " + (vars.userspace ? "--user " : "");
|
||||
var execstr = launcherstr + "daemon-reload";
|
||||
exec(execstr, things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, stderr);
|
||||
if (err) { fn(err); return; }
|
||||
//console.log((stdout||'').trim());
|
||||
var execstr = launcherstr + "enable " + launchername;
|
||||
exec(execstr, things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, !/Created symlink/.test(stderr||''));
|
||||
if (err) { fn(err); return; }
|
||||
//console.log((stdout||'').trim());
|
||||
var execstr = launcherstr + "restart " + launchername;
|
||||
exec(execstr, things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, stderr);
|
||||
if (err) { fn(err); return; }
|
||||
//console.log((stdout||'').trim());
|
||||
setTimeout(function () {
|
||||
var execstr = launcherstr + "status " + launchername;
|
||||
exec(execstr, things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, stderr);
|
||||
if (err) { fn(err); return; }
|
||||
if (!/active.*running/i.test(stdout)) {
|
||||
err = new Error("systemd failed to start '" + launchername + "'");
|
||||
}
|
||||
if (err) { fn(err); return; }
|
||||
//console.log((stdout||'').trim());
|
||||
fn(null);
|
||||
});
|
||||
}, 1.25 * 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch(e) {
|
||||
console.error("'" + launcher + "' error:");
|
||||
console.error(e);
|
||||
if (fn) { fn(e); return; }
|
||||
}
|
||||
}
|
||||
, 'reg.exe': function () {
|
||||
if (!vars.userspace) {
|
||||
console.warn("sysetm-level, privileged services are not yet supported on windows");
|
||||
}
|
||||
vars.telebitNode += '.exe';
|
||||
var cmd = 'reg.exe add "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"'
|
||||
+ ' /V "Telebit" /t REG_SZ /D '
|
||||
+ '"' + things.argv[0] + ' /c ' // something like C:\\Program Files (x64)\nodejs\node.exe
|
||||
+ [ path.join(__dirname, 'bin/telebitd.js')
|
||||
, 'daemon'
|
||||
, '--config'
|
||||
, path.join(os.homedir(), '.config/telebit/telebitd.yml')
|
||||
].join(' ')
|
||||
+ '" /F'
|
||||
;
|
||||
exec(cmd, things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, stderr);
|
||||
if (err) { fn(err); return; }
|
||||
// need to start it for the first time ourselves
|
||||
run(null, 'node');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function run(err, launcher) {
|
||||
if (err) {
|
||||
console.error("No luck with '" + launcher + "', trying a child process instead...");
|
||||
console.error(err);
|
||||
launcher = 'node';
|
||||
}
|
||||
|
||||
if (launchers[launcher]) {
|
||||
// console.log('Launching with launcher ' + launcher);
|
||||
mkdirp.sync(path.join(vars.telebitPath, 'bin'));
|
||||
mkdirp.sync(vars.TELEBIT_LOG_DIR);
|
||||
mkdirp.sync(vars.TELEBIT_SOCK_DIR);
|
||||
launchers[launcher]();
|
||||
return;
|
||||
} else {
|
||||
console.error("No launcher handler for '" + launcher+ "'");
|
||||
}
|
||||
}
|
||||
|
||||
if (things.launcher) {
|
||||
if ('string' === typeof things.launcher) {
|
||||
run(null, things.launcher);
|
||||
return;
|
||||
}
|
||||
if ('function' === typeof things.launcher) {
|
||||
things._vars = vars;
|
||||
things._userspace = vars.userspace;
|
||||
things.launcher(things);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// could have used "command-exists" but I'm trying to stay low-dependency
|
||||
// os.platform(), os.type()
|
||||
if (!/^win/i.test(os.platform())) {
|
||||
if (/^darwin/i.test(os.platform())) {
|
||||
exec('command -v launchctl', things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, stderr);
|
||||
run(err, 'launchctl');
|
||||
});
|
||||
} else {
|
||||
exec('command -v systemctl', things._execOpts, function (err, stdout, stderr) {
|
||||
err = getError(err, stderr);
|
||||
run(err, 'systemctl');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac
|
||||
// wininit? regedit? SCM?
|
||||
// REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe"
|
||||
// https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/
|
||||
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add
|
||||
// https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools
|
||||
// utils.elevate
|
||||
// https://github.com/CatalystCode/windows-registry-node
|
||||
exec('where reg.exe', things._execOpts, function (err, stdout, stderr) {
|
||||
//console.log((stdout||'').trim());
|
||||
if (stderr) {
|
||||
console.error(stderr);
|
||||
}
|
||||
run(err, 'reg.exe');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (module === require.main) {
|
||||
module.exports.install({
|
||||
argv: process.argv
|
||||
, env: process.env
|
||||
}, function (err) {
|
||||
if (err) { console.error(err); return; }
|
||||
console.log("Telebit launched, or so it seems.");
|
||||
});
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
#!/bin/bash
|
||||
#<pre><code>
|
||||
|
||||
# This script does exactly 3 things for 1 good reason:
|
||||
#
|
||||
# What this does:
|
||||
#
|
||||
# 1. Detects either curl or wget and wraps them in helpers
|
||||
# 2. Exports the helpers for the real installer
|
||||
# 3. Downloads and runs the real installer
|
||||
#
|
||||
# Why
|
||||
#
|
||||
# 1. 'curl <smth> | bash -- some args here` breaks interactive input
|
||||
# See https://stackoverflow.com/questions/16854041/bash-read-is-being-skipped-when-run-from-curl-pipe
|
||||
#
|
||||
# 2. It also has practical risks of running a partially downloaded script, which could be dangeresque
|
||||
# See https://news.ycombinator.com/item?id=12767636
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
###############################
|
||||
# #
|
||||
# http_get #
|
||||
# boilerplate for curl / wget #
|
||||
# #
|
||||
###############################
|
||||
|
||||
# See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh
|
||||
|
||||
export _my_http_get=""
|
||||
export _my_http_opts=""
|
||||
export _my_http_out=""
|
||||
|
||||
detect_http_get()
|
||||
{
|
||||
set +e
|
||||
if type -p curl >/dev/null 2>&1; then
|
||||
_my_http_get="curl"
|
||||
_my_http_opts="-fsSL"
|
||||
_my_http_out="-o"
|
||||
elif type -p wget >/dev/null 2>&1; then
|
||||
_my_http_get="wget"
|
||||
_my_http_opts="--quiet"
|
||||
_my_http_out="-O"
|
||||
else
|
||||
echo "Aborted, could not find curl or wget"
|
||||
return 7
|
||||
fi
|
||||
set -e
|
||||
}
|
||||
|
||||
http_get()
|
||||
{
|
||||
$_my_http_get $_my_http_opts $_my_http_out "$2" "$1"
|
||||
touch "$2"
|
||||
}
|
||||
|
||||
http_bash()
|
||||
{
|
||||
_http_bash_url=$1
|
||||
_http_bash_args=${2:-}
|
||||
_http_bash_tmp=$(mktemp)
|
||||
$_my_http_get $_my_http_opts $_my_http_out "$_http_bash_tmp" "$_http_bash_url"
|
||||
bash "$_http_bash_tmp" $_http_bash_args; rm "$_http_bash_tmp"
|
||||
}
|
||||
|
||||
detect_http_get
|
||||
export -f http_get
|
||||
export -f http_bash
|
||||
|
||||
###############################
|
||||
## END HTTP_GET ##
|
||||
###############################
|
||||
|
||||
if [ -n "${TELEBIT_VERSION:-}" ]; then
|
||||
echo 'TELEBIT_VERSION='${TELEBIT_VERSION}
|
||||
fi
|
||||
TELEBIT_VERSION=${TELEBIT_VERSION:-master}
|
||||
if [ -e "usr/share/install_helper.sh" ]; then
|
||||
bash usr/share/install_helper.sh "$@"
|
||||
else
|
||||
http_bash https://git.coolaj86.com/coolaj86/telebit.js/raw/branch/$TELEBIT_VERSION/usr/share/install_helper.sh "$@"
|
||||
fi
|
|
@ -1,560 +0,0 @@
|
|||
#!/bin/bash
|
||||
#<pre><code>
|
||||
|
||||
# What does this do.. and why?
|
||||
# (and why is it so complicated?)
|
||||
#
|
||||
# What this does
|
||||
#
|
||||
# 1. Sets some vars and asks some questions
|
||||
# 2. Installs everything into a single place
|
||||
# (inculding deps like node.js, with the correct version)
|
||||
# 3. Depending on OS, creates a user for the service
|
||||
# 4. Depending on OS, register with system launcher
|
||||
#
|
||||
# Why
|
||||
#
|
||||
# So that you can get a fully configured, running product,
|
||||
# with zero manual configuration in a matter of seconds -
|
||||
# and have an uninstall that's just as easy.
|
||||
#
|
||||
# Why so complicated?
|
||||
#
|
||||
# To support nuance differences between various versions of
|
||||
# Linux, macOS, and Android, including whether it's being
|
||||
# installed with user privileges, as root, wit a system user
|
||||
# system daemon launcher, etc. Also, this is designed to be
|
||||
# reusable with many apps and services, so it's very variabled...
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
### http_bash exported by get.sh
|
||||
|
||||
TELEBIT_DEBUG=${TELEBIT_DEBUG:-}
|
||||
|
||||
# NOTE: On OS X logname works from a pipe, but on Linux it does not
|
||||
my_logname=$(who am i | awk '{print $1}')
|
||||
#my_logname=${my_logname:-$(logname)}
|
||||
#my_logname=${my_logname:-$SUDO_USER}
|
||||
if [ -n "$my_logname" ] && [ "$my_logname" != "$(id -u -n)" ]; then
|
||||
echo "WARNING:"
|
||||
echo " You are logged in as '$(logname)' but acting as '$(id -u -n)'."
|
||||
echo " If the installation is not successful please log in as '$(id -u -n)' directly."
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
if [ -n "${TELEBIT_DEBUG:-}" ]; then
|
||||
echo 'TELEBIT_DEBUG='${TELEBIT_DEBUG}
|
||||
fi
|
||||
if [ -n "${TELEBIT_PATH:-}" ]; then
|
||||
echo 'TELEBIT_PATH='${TELEBIT_PATH}
|
||||
fi
|
||||
if [ -n "${TELEBIT_USERSPACE:-}" ]; then
|
||||
echo 'TELEBIT_USERSPACE='${TELEBIT_USERSPACE}
|
||||
fi
|
||||
if [ -n "${TELEBIT_USER:-}" ]; then
|
||||
echo 'TELEBIT_USER='${TELEBIT_USER}
|
||||
fi
|
||||
if [ -n "${TELEBIT_GROUP:-}" ]; then
|
||||
echo 'TELEBIT_GROUP='${TELEBIT_GROUP}
|
||||
fi
|
||||
TELEBIT_VERSION=${TELEBIT_VERSION:-master}
|
||||
TELEBIT_USERSPACE=${TELEBIT_USERSPACE:-no}
|
||||
my_email=${1:-}
|
||||
my_relay=${2:-}
|
||||
my_servernames=${3:-}
|
||||
my_secret=${4:-}
|
||||
|
||||
cur_user="$(id -u -n)"
|
||||
TELEBIT_USER="${TELEBIT_USER:-$cur_user}"
|
||||
|
||||
cur_group="$(id -g -n)"
|
||||
TELEBIT_GROUP="${TELEBIT_GROUP:-$cur_group}"
|
||||
|
||||
my_app_pkg_name="cloud.telebit.remote"
|
||||
my_app="telebit"
|
||||
my_daemon="telebitd"
|
||||
my_bin="telebit.js"
|
||||
my_name="Telebit Remote"
|
||||
my_repo="telebit.js"
|
||||
my_root=${my_root:-} # todo better install script
|
||||
soft_sudo_cmd="sudo"
|
||||
soft_sudo_cmde="sudo "
|
||||
exec 3<>/dev/tty
|
||||
read_cmd="read -u 3"
|
||||
# TODO detect if rsync is available and use rsync -a (more portable)
|
||||
rsync_cmd="cp -pPR"
|
||||
|
||||
set +e
|
||||
my_edit=$(basename "${EDITOR:-}")
|
||||
if [ -z "$my_edit" ]; then
|
||||
my_edit=$(basename "$(type -p edit)")
|
||||
fi
|
||||
if [ -z "$my_edit" ]; then
|
||||
my_edit=$(basename "$(type -p nano)")
|
||||
fi
|
||||
if [ -z "$my_edit" ]; then
|
||||
my_edit=$(basename "$(type -p vim)")
|
||||
fi
|
||||
if [ -z "$my_edit" ]; then
|
||||
my_edit=$(basename "$(type -p vi)")
|
||||
fi
|
||||
if [ -z "$my_edit" ]; then
|
||||
my_edit="nano"
|
||||
fi
|
||||
set -e
|
||||
|
||||
if [ "root" == $(whoami) ] || [ 0 == $(id -u) ]; then
|
||||
soft_sudo_cmd=" "
|
||||
soft_sudo_cmde=""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
TELEBIT_REAL_PATH=${TELEBIT_PATH:-}
|
||||
|
||||
if [ $(id -u) -ne 0 ] && [ "$TELEBIT_USER" == "$cur_user" ]; then
|
||||
TELEBIT_USERSPACE="yes"
|
||||
if [ -z "${TELEBIT_REAL_PATH:-}" ]; then
|
||||
TELEBIT_REAL_PATH=$HOME/Applications/$my_app
|
||||
fi
|
||||
else
|
||||
TELEBIT_USERSPACE="no"
|
||||
if [ -z "${TELEBIT_REAL_PATH:-}" ]; then
|
||||
TELEBIT_REAL_PATH=/opt/$my_app
|
||||
fi
|
||||
fi
|
||||
TELEBIT_PATH="$TELEBIT_REAL_PATH"
|
||||
TELEBIT_TMP="$TELEBIT_REAL_PATH"
|
||||
# this works slightly differently between bsd (macOS) and gnu mktemp
|
||||
# bsd requires the Xes for templates while GNU uses them literally
|
||||
my_tmp="$(mktemp -d -t telebit.XXXXXXXX)"
|
||||
#TELEBIT_TMP="$my_tmp/telebit"
|
||||
|
||||
echo "Installing $my_name to '$TELEBIT_REAL_PATH'"
|
||||
# v10.2+ has much needed networking fixes, but breaks ursa. v9.x has severe networking bugs. v8.x has working ursa, but requires tls workarounds"
|
||||
NODEJS_VER="${NODEJS_VER:-v10.6}"
|
||||
export NODEJS_VER
|
||||
export NODE_PATH="$TELEBIT_TMP/lib/node_modules"
|
||||
export NPM_CONFIG_PREFIX="$TELEBIT_TMP"
|
||||
# this comes last for security
|
||||
export PATH="$PATH:$TELEBIT_REAL_PATH/bin"
|
||||
sleep 0.25
|
||||
real_sudo_cmd=$soft_sudo_cmd
|
||||
real_sudo_cmde=$soft_sudo_cmde
|
||||
|
||||
set +e
|
||||
mkdir -p $my_tmp "$TELEBIT_REAL_PATH" "$TELEBIT_REAL_PATH/etc" "$TELEBIT_REAL_PATH/var/log" 2>/dev/null && \
|
||||
chown -R $(id -u -n):$(id -g -n) $my_tmp "$TELEBIT_REAL_PATH" 2>/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
soft_sudo_cmd=" "
|
||||
soft_sudo_cmde=""
|
||||
else
|
||||
$soft_sudo_cmd mkdir -p $my_tmp "$TELEBIT_REAL_PATH" "$TELEBIT_REAL_PATH/etc" "$TELEBIT_REAL_PATH/var/log"
|
||||
$soft_sudo_cmd chown -R $(id -u -n):$(id -g -n) $my_tmp "$TELEBIT_REAL_PATH"
|
||||
fi
|
||||
set -e
|
||||
|
||||
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " - installing node.js runtime to '$TELEBIT_REAL_PATH'..."
|
||||
http_bash https://git.coolaj86.com/coolaj86/node-installer.sh/raw/branch/master/install.sh --no-dev-deps
|
||||
else
|
||||
echo -n "."
|
||||
#bash -c 'while true; do echo -n "."; sleep 2; done' 2>/dev/null &
|
||||
#_my_pid=$!
|
||||
http_bash https://git.coolaj86.com/coolaj86/node-installer.sh/raw/branch/master/install.sh --no-dev-deps >/dev/null 2>/dev/null
|
||||
#kill $_my_pid >/dev/null 2>/dev/null
|
||||
fi
|
||||
|
||||
#
|
||||
# TODO create "upgrade" script and run that instead
|
||||
#
|
||||
|
||||
my_node="$TELEBIT_REAL_PATH/bin/node"
|
||||
tmp_node="$TELEBIT_TMP/bin/node"
|
||||
my_npm="$my_node $TELEBIT_TMP/bin/npm"
|
||||
tmp_npm="$tmp_node $TELEBIT_TMP/bin/npm"
|
||||
|
||||
#https://git.coolaj86.com/coolaj86/telebit.js.git
|
||||
#https://git.coolaj86.com/coolaj86/telebit.js/archive/:tree:.tar.gz
|
||||
#https://git.coolaj86.com/coolaj86/telebit.js/archive/:tree:.zip
|
||||
set +e
|
||||
my_unzip=$(type -p unzip)
|
||||
my_tar=$(type -p tar)
|
||||
# TODO extract to temporary directory, configure, copy etc, replace
|
||||
if [ -n "$my_unzip" ]; then
|
||||
rm -f $my_tmp/$my_app-$TELEBIT_VERSION.zip
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " - installing telebit zip to '$TELEBIT_REAL_PATH'"
|
||||
fi
|
||||
echo -n "."
|
||||
#bash -c 'while true; do echo -n "."; sleep 2; done' 2>/dev/null &
|
||||
#_my_pid=$!
|
||||
http_get https://git.coolaj86.com/coolaj86/$my_repo/archive/$TELEBIT_VERSION.zip $my_tmp/$my_app-$TELEBIT_VERSION.zip
|
||||
#kill $_my_pid >/dev/null 2>/dev/null
|
||||
# -o means overwrite, and there is no option to strip
|
||||
$my_unzip -o $my_tmp/$my_app-$TELEBIT_VERSION.zip -d $my_tmp/ >/dev/null
|
||||
$rsync_cmd $my_tmp/$my_repo/* $TELEBIT_TMP/ > /dev/null
|
||||
rm -rf $my_tmp/$my_repo
|
||||
elif [ -n "$my_tar" ]; then
|
||||
rm -f $my_tmp/$my_app-$TELEBIT_VERSION.tar.gz
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " - installing telebit tar.gz to '$TELEBIT_REAL_PATH'"
|
||||
fi
|
||||
echo -n "."
|
||||
#bash -c 'while true; do echo -n "."; sleep 2; done' 2>/dev/null &
|
||||
#_my_pid=$!
|
||||
http_get https://git.coolaj86.com/coolaj86/$my_repo/archive/$TELEBIT_VERSION.tar.gz $my_tmp/$my_app-$TELEBIT_VERSION.tar.gz
|
||||
#kill $_my_pid >/dev/null 2>/dev/null
|
||||
$my_tar -xzf $my_tmp/$my_app-$TELEBIT_VERSION.tar.gz --strip 1 -C $TELEBIT_TMP/ >/dev/null
|
||||
else
|
||||
echo "Neither tar nor unzip found. Abort."
|
||||
exit 13
|
||||
fi
|
||||
set -e
|
||||
|
||||
#
|
||||
# TODO create slim packages that contain all the deps on each os and cpu
|
||||
#
|
||||
pushd $TELEBIT_TMP >/dev/null
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " - installing telebit npm dependencies to '$TELEBIT_REAL_PATH'..."
|
||||
else
|
||||
echo -n "."
|
||||
fi
|
||||
$tmp_npm install >/dev/null 2>/dev/null &
|
||||
# ursa is now an entirely optional dependency for key generation
|
||||
# but very much needed on ARM devices
|
||||
$tmp_npm install ursa >/dev/null 2>/dev/null &
|
||||
tmp_npm_pid=$!
|
||||
while [ -n "$tmp_npm_pid" ]; do
|
||||
sleep 2
|
||||
echo -n "."
|
||||
kill -s 0 $tmp_npm_pid >/dev/null 2>/dev/null || tmp_npm_pid=""
|
||||
done
|
||||
popd >/dev/null
|
||||
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " - configuring telebit..."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
###############################################
|
||||
#
|
||||
# TODO convert to node script
|
||||
#
|
||||
# Now that node is installed and the telebit
|
||||
# packeage is downloaded, everything can be
|
||||
# run from node, except things requiring sudo
|
||||
#
|
||||
###############################################
|
||||
|
||||
# telebit remote
|
||||
echo '#!/bin/bash' > "$TELEBIT_TMP/bin/$my_app"
|
||||
echo "$my_node $TELEBIT_REAL_PATH/bin/$my_bin "'"$@"' >> "$TELEBIT_TMP/bin/$my_app"
|
||||
chmod a+x "$TELEBIT_TMP/bin/$my_app"
|
||||
|
||||
# telebit daemon
|
||||
echo '#!/bin/bash' > "$TELEBIT_TMP/bin/$my_daemon"
|
||||
echo "$my_node $TELEBIT_REAL_PATH/bin/$my_daemon.js daemon "'"$@"' >> "$TELEBIT_TMP/bin/$my_daemon"
|
||||
chmod a+x "$TELEBIT_TMP/bin/$my_daemon"
|
||||
|
||||
# Create uninstall script based on the install script variables
|
||||
cat << EOF > $TELEBIT_TMP/bin/${my_app}_uninstall
|
||||
#!/bin/bash
|
||||
set -x
|
||||
if [ "$(type -p launchctl)" ]; then
|
||||
sudo launchctl unload -w /Library/LaunchDaemons/${my_app_pkg_name}.plist
|
||||
sudo rm -f /Library/LaunchDaemons/${my_app_pkg_name}.plist
|
||||
|
||||
launchctl unload -w $HOME/Library/LaunchAgents/${my_app_pkg_name}.plist
|
||||
rm -f $HOME/Library/LaunchAgents/${my_app_pkg_name}.plist
|
||||
fi
|
||||
if [ "$(type -p systemctl)" ]; then
|
||||
systemctl --user disable $my_app >/dev/null
|
||||
systemctl --user stop $my_app
|
||||
rm -f $HOME/.config/systemd/user/$my_app.service
|
||||
|
||||
sudo systemctl disable $my_app >/dev/null
|
||||
sudo systemctl stop $my_app
|
||||
sudo rm -f /etc/systemd/system/$my_app.service
|
||||
fi
|
||||
sudo rm -rf $TELEBIT_REAL_PATH /usr/local/bin/$my_app
|
||||
sudo rm -rf $TELEBIT_REAL_PATH /usr/local/bin/$my_daemon
|
||||
rm -rf $HOME/.config/$my_app $HOME/.local/share/$my_app
|
||||
EOF
|
||||
chmod a+x $TELEBIT_TMP/bin/${my_app}_uninstall
|
||||
|
||||
#set +e
|
||||
#if type -p setcap >/dev/null 2>&1; then
|
||||
# #echo "Setting permissions to allow $my_app to run on port 80 and port 443 without sudo or root"
|
||||
# echo " > ${real_sudo_cmde}setcap cap_net_bind_service=+ep $TELEBIT_REAL_PATH/bin/node"
|
||||
# $real_sudo_cmd setcap cap_net_bind_service=+ep $TELEBIT_REAL_PATH/bin/node
|
||||
#fi
|
||||
#set -e
|
||||
|
||||
my_skip=""
|
||||
set +e
|
||||
# TODO for macOS https://apple.stackexchange.com/questions/286749/how-to-add-a-user-from-the-command-line-in-macos
|
||||
# TODO do stuff for groups too
|
||||
# TODO add ending $
|
||||
if type -p dscl >/dev/null 2>/dev/null; then
|
||||
if [ -n "$(dscl . list /users | grep ^$TELEBIT_USER)" ] && [ -n "$(dscl . list /groups | grep ^$TELEBIT_GROUP)" ]; then
|
||||
my_skip="yes"
|
||||
fi
|
||||
elif [ -n "$(cat $my_root/etc/passwd | grep $TELEBIT_USER)" ] && [ -n "$(cat $my_root/etc/group | grep $TELEBIT_GROUP)" ]; then
|
||||
my_skip="yes"
|
||||
fi
|
||||
|
||||
if [ -z "$my_skip" ]; then
|
||||
if type -p adduser >/dev/null 2>/dev/null; then
|
||||
$real_sudo_cmd adduser --home $TELEBIT_REAL_PATH --gecos '' --disabled-password $TELEBIT_USER >/dev/null 2>&1
|
||||
#TELEBIT_USER=$my_app_name
|
||||
TELEBIT_GROUP=$TELEBIT_USER
|
||||
elif [ -n "$(cat /etc/passwd | grep www-data:)" ]; then
|
||||
# Linux (Ubuntu)
|
||||
TELEBIT_USER=www-data
|
||||
TELEBIT_GROUP=www-data
|
||||
elif [ -n "$(cat /etc/passwd | grep _www:)" ]; then
|
||||
# Mac
|
||||
TELEBIT_USER=_www
|
||||
TELEBIT_GROUP=_www
|
||||
else
|
||||
# Unsure
|
||||
TELEBIT_USER=$(id -u -n) # $(whoami)
|
||||
TELEBIT_GROUP=$(id -g -n)
|
||||
fi
|
||||
fi
|
||||
set -e
|
||||
|
||||
export TELEBIT_USER
|
||||
export TELEBIT_GROUP
|
||||
export TELEBIT_PATH
|
||||
export TELEBIT_CONFIG=$HOME/.config/$my_app/$my_app.yml
|
||||
# TODO check both expected sock paths in client by default
|
||||
if [ "yes" == "$TELEBIT_USERSPACE" ]; then
|
||||
TELEBIT_TMP_CONFIGD=$HOME/.config/$my_app/$my_daemon.yml
|
||||
TELEBITD_CONFIG=$HOME/.config/$my_app/$my_daemon.yml
|
||||
TELEBIT_LOG_DIR=${TELEBIT_LOG_DIR:-$HOME/.local/share/$my_app/var/log/}
|
||||
TELEBIT_SOCK_DIR=${TELEBIT_SOCK_DIR:-$HOME/.local/share/$my_app/var/run/}
|
||||
TELEBIT_SOCK=${TELEBIT_SOCK:-$HOME/.local/share/$my_app/var/run/$my_app.sock}
|
||||
else
|
||||
TELEBIT_TMP_CONFIGD=$TELEBIT_TMP/etc/$my_daemon.yml
|
||||
TELEBITD_CONFIG=$TELEBIT_REAL_PATH/etc/$my_daemon.yml
|
||||
TELEBIT_LOG_DIR=${TELEBIT_LOG_DIR:-$TELEBIT_REAL_PATH/var/log/}
|
||||
TELEBIT_SOCK_DIR=${TELEBIT_SOCK_DIR:-$TELEBIT_REAL_PATH/var/run/}
|
||||
TELEBIT_SOCK=${TELEBIT_SOCK:-$TELEBIT_REAL_PATH/var/run/$my_app.sock}
|
||||
fi
|
||||
export TELEBITD_CONFIG
|
||||
export TELEBIT_SOCK
|
||||
export TELEBIT_NODE=$TELEBIT_REAL_PATH/bin/node
|
||||
export TELEBIT_NPM=$TELEBIT_REAL_PATH/bin/npm
|
||||
export TELEBIT_BIN=$TELEBIT_REAL_PATH/bin/telebit
|
||||
export TELEBITD_BIN=$TELEBIT_REAL_PATH/bin/telebitd
|
||||
export TELEBIT_JS=$TELEBIT_REAL_PATH/bin/telebit.js
|
||||
export TELEBITD_JS=$TELEBIT_REAL_PATH/bin/telebitd.js
|
||||
export TELEBIT_LOG_DIR
|
||||
export TELEBIT_SOCK_DIR
|
||||
export NODE_PATH="$TELEBIT_REAL_PATH/lib/node_modules"
|
||||
export NPM_CONFIG_PREFIX="$TELEBIT_REAL_PATH"
|
||||
|
||||
$my_node $TELEBIT_TMP/usr/share/template-launcher.js
|
||||
|
||||
# TODO don't create this in TMP_PATH if it exists in TELEBIT_REAL_PATH
|
||||
mkdir -p "$(dirname $TELEBIT_TMP_CONFIGD)"
|
||||
if [ ! -e "$TELEBITD_CONFIG" ]; then
|
||||
|
||||
echo "sock: $TELEBIT_SOCK" >> "$TELEBIT_TMP_CONFIGD"
|
||||
echo "root: $TELEBIT_REAL_PATH" >> "$TELEBIT_TMP_CONFIGD"
|
||||
cat $TELEBIT_REAL_PATH/usr/share/$my_daemon.tpl.yml >> "$TELEBIT_TMP_CONFIGD"
|
||||
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname $TELEBIT_CONFIG)"
|
||||
if [ ! -e "$TELEBIT_CONFIG" ]; then
|
||||
|
||||
echo "sock: $TELEBIT_SOCK" >> "$TELEBIT_CONFIG"
|
||||
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# TODO
|
||||
# Backup final directory, if it exists
|
||||
# Move everything over to final directory
|
||||
# Restore config files, if they exist
|
||||
# rewrite system service file with real variables
|
||||
|
||||
# This should only affect non-USERSPACE installs
|
||||
#echo "${soft_sudo_cmde}chown -R $TELEBIT_USER '$TELEBIT_REAL_PATH'
|
||||
$soft_sudo_cmd mkdir -p $TELEBIT_LOG_DIR
|
||||
$soft_sudo_cmd mkdir -p $TELEBIT_SOCK_DIR
|
||||
$soft_sudo_cmd chown -R $TELEBIT_USER "$TELEBIT_REAL_PATH"
|
||||
|
||||
# $HOME/.config/systemd/user/
|
||||
# %h/.config/telebit/telebit.yml
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " - adding $my_app as a system service"
|
||||
fi
|
||||
# TODO detect with type -p
|
||||
my_system_launcher=""
|
||||
my_app_launchd_service=""
|
||||
if [ -d "/Library/LaunchDaemons" ]; then
|
||||
my_system_launcher="launchd"
|
||||
my_sudo_cmde="$real_sudo_cmde"
|
||||
my_sudo_cmd="$real_sudo_cmd"
|
||||
|
||||
|
||||
if [ "yes" == "$TELEBIT_USERSPACE" ]; then
|
||||
my_app_launchd_service_skel="etc/skel/Library/LaunchAgents/${my_app_pkg_name}.plist"
|
||||
my_app_launchd_service="$HOME/Library/LaunchAgents/${my_app_pkg_name}.plist"
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > $rsync_cmd $TELEBIT_REAL_PATH/usr/share/dist/$my_app_launchd_service $my_app_launchd_service"
|
||||
fi
|
||||
mkdir -p $HOME/Library/LaunchAgents
|
||||
$rsync_cmd "$TELEBIT_REAL_PATH/usr/share/dist/$my_app_launchd_service_skel" "$my_app_launchd_service"
|
||||
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > chown $(id -u -n):$(id -g -n) $my_app_launchd_service"
|
||||
fi
|
||||
chown $(id -u -n):$(id -g -n) "$my_app_launchd_service"
|
||||
my_sudo_cmd=""
|
||||
my_sudo_cmde=""
|
||||
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > launchctl unload -w $my_app_launchd_service >/dev/null 2>/dev/null"
|
||||
launchctl unload -w "$my_app_launchd_service" >/dev/null 2>/dev/null
|
||||
fi
|
||||
else
|
||||
my_app_launchd_service_skel="usr/share/dist/Library/LaunchDaemons/${my_app_pkg_name}.plist"
|
||||
my_app_launchd_service="$my_root/Library/LaunchDaemons/${my_app_pkg_name}.plist"
|
||||
echo " > ${real_sudo_cmde}$rsync_cmd $TELEBIT_REAL_PATH/usr/share/dist/$my_app_launchd_service $my_app_launchd_service"
|
||||
$real_sudo_cmd $rsync_cmd "$TELEBIT_REAL_PATH/usr/share/dist/$my_app_launchd_service_skel" "$my_app_launchd_service"
|
||||
|
||||
echo " > ${real_sudo_cmde}chown root:wheel $my_app_launchd_service"
|
||||
$real_sudo_cmd chown root:wheel "$my_app_launchd_service"
|
||||
|
||||
echo " > ${real_sudo_cmde}launchctl unload -w $my_app_launchd_service >/dev/null 2>/dev/null"
|
||||
$real_sudo_cmd launchctl unload -w "$my_app_launchd_service" >/dev/null 2>/dev/null
|
||||
fi
|
||||
|
||||
elif [ -d "$my_root/etc/systemd/system" ]; then
|
||||
my_system_launcher="systemd"
|
||||
|
||||
if [ "yes" == "$TELEBIT_USERSPACE" ]; then
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > $rsync_cmd $TELEBIT_REAL_PATH/usr/share/dist/etc/skel/.config/systemd/user/$my_app.service $HOME/.config/systemd/user/$my_app.service"
|
||||
fi
|
||||
mkdir -p $HOME/.config/systemd/user
|
||||
$rsync_cmd "$TELEBIT_REAL_PATH/usr/share/dist/etc/skel/.config/systemd/user/$my_app.service" "$HOME/.config/systemd/user/$my_app.service"
|
||||
else
|
||||
echo " > ${real_sudo_cmde}$rsync_cmd $TELEBIT_REAL_PATH/usr/share/dist/etc/systemd/system/$my_app.service /etc/systemd/system/$my_app.service"
|
||||
$real_sudo_cmd $rsync_cmd "$TELEBIT_REAL_PATH/usr/share/dist/etc/systemd/system/$my_app.service" "/etc/systemd/system/$my_app.service"
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
###############################
|
||||
# Actually Launch the Service #
|
||||
###############################
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo ""
|
||||
fi
|
||||
if [ "launchd" == "$my_system_launcher" ]; then
|
||||
|
||||
if [ "yes" == "$TELEBIT_USERSPACE" ]; then
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > launchctl load -w $my_app_launchd_service"
|
||||
else
|
||||
echo -n "."
|
||||
fi
|
||||
launchctl load -w "$my_app_launchd_service"
|
||||
else
|
||||
echo " > ${real_sudo_cmde}launchctl load -w $my_app_launchd_service"
|
||||
$real_sudo_cmd launchctl load -w "$my_app_launchd_service"
|
||||
fi
|
||||
sleep 2; # give it time to start
|
||||
|
||||
elif [ "systemd" == "$my_system_launcher" ]; then
|
||||
|
||||
if [ "yes" == "$TELEBIT_USERSPACE" ]; then
|
||||
# https://wiki.archlinux.org/index.php/Systemd/User
|
||||
# sudo loginctl enable-linger username
|
||||
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > systemctl --user enable $my_app"
|
||||
else
|
||||
echo -n "."
|
||||
fi
|
||||
systemctl --user daemon-reload
|
||||
# enable also puts success output to stderr... why?
|
||||
systemctl --user enable $my_app >/dev/null 2>/dev/null
|
||||
#echo " > systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer"
|
||||
#systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > systemctl --user start $my_app"
|
||||
fi
|
||||
systemctl --user stop $my_app >/dev/null 2>/dev/null
|
||||
systemctl --user start $my_app >/dev/null
|
||||
sleep 2; # give it time to start
|
||||
_is_running=$(systemctl --user status --no-pager $my_app 2>/dev/null | grep "active.*running")
|
||||
if [ -z "$_is_running" ]; then
|
||||
echo "Something went wrong:"
|
||||
systemctl --user status --no-pager $my_app
|
||||
exit 1
|
||||
fi
|
||||
echo -n "."
|
||||
else
|
||||
|
||||
$real_sudo_cmd systemctl daemon-reload
|
||||
echo " > ${real_sudo_cmde}systemctl enable $my_app"
|
||||
$real_sudo_cmd systemctl enable $my_app >/dev/null
|
||||
echo " > ${real_sudo_cmde}systemctl start $my_app"
|
||||
$real_sudo_cmd systemctl daemon-reload
|
||||
$real_sudo_cmd systemctl restart $my_app
|
||||
sleep 2; # give it time to start
|
||||
$real_sudo_cmd systemctl status --no-pager $my_app
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
echo "Run the service manually (we couldn't detect your system service to do that automatically):"
|
||||
echo ""
|
||||
echo " $TELEBITD_BIN --config $TELEBITD_CONFIG"
|
||||
echo " ~/$my_app --config $TELEBIT_CONFIG"
|
||||
|
||||
fi
|
||||
|
||||
# NOTE: ln -sf *should* replace an existing link... but sometimes it doesn't, hence rm -f
|
||||
if [ "yes" == "$TELEBIT_USERSPACE" ]; then
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > ${real_sudo_cmde}ln -sf $TELEBIT_REAL_PATH/bin/$my_app /usr/local/bin/$my_app"
|
||||
fi
|
||||
rm -f /usr/local/bin/$my_app 2>/dev/null || true
|
||||
ln -sf $TELEBIT_REAL_PATH/bin/$my_app /usr/local/bin/$my_app 2>/dev/null || true
|
||||
else
|
||||
echo " > ${real_sudo_cmde}ln -sf $TELEBIT_REAL_PATH/bin/$my_app /usr/local/bin/$my_app"
|
||||
rm -f /usr/local/bin/$my_app 2>/dev/null || \
|
||||
$real_sudo_cmd rm -f /usr/local/bin/$my_app
|
||||
ln -sf $TELEBIT_REAL_PATH/bin/$my_app /usr/local/bin/$my_app 2>/dev/null || \
|
||||
$real_sudo_cmd ln -sf $TELEBIT_REAL_PATH/bin/$my_app /usr/local/bin/$my_app
|
||||
# telebitd
|
||||
echo " > ${real_sudo_cmde}ln -sf $TELEBIT_REAL_PATH/bin/$my_daemon /usr/local/bin/$my_daemon"
|
||||
rm -f $TELEBIT_REAL_PATH/bin/$my_daemon || $real_sudo_cmd rm -f $TELEBIT_REAL_PATH/bin/$my_daemon
|
||||
ln -sf $TELEBIT_REAL_PATH/bin/$my_daemon /usr/local/bin/$my_daemon || \
|
||||
$real_sudo_cmd ln -sf $TELEBIT_REAL_PATH/bin/$my_daemon /usr/local/bin/$my_daemon
|
||||
fi
|
||||
rm -f $HOME/$my_app; ln -s $TELEBIT_REAL_PATH/bin/$my_app $HOME/
|
||||
|
||||
|
||||
if [ -n "${TELEBIT_DEBUG}" ]; then
|
||||
echo " > telebit init --tty"
|
||||
echo ""
|
||||
fi
|
||||
sleep 0.25
|
||||
|
||||
echo ""
|
||||
$TELEBIT_REAL_PATH/bin/node $TELEBIT_REAL_PATH/bin/telebit.js init --tty
|
|
@ -1,46 +0,0 @@
|
|||
echo ""
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Launcher Configuration "
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
my_stopper=""
|
||||
if [ "systemd" == "$my_system_launcher" ]; then
|
||||
|
||||
my_stopper="${real_sudo_cmde}systemctl stop $my_app"
|
||||
echo "Edit the config and restart, if desired:"
|
||||
echo ""
|
||||
echo " ${real_sudo_cmde}$my_edit $TELEBITD_CONFIG"
|
||||
echo " ${real_sudo_cmde}systemctl restart $my_app"
|
||||
echo ""
|
||||
echo "Or disabled the service and start manually:"
|
||||
echo ""
|
||||
echo " ${real_sudo_cmde}systemctl stop $my_app"
|
||||
echo " ${real_sudo_cmde}systemctl disable $my_app"
|
||||
echo " $my_daemon --config $TELEBITD_CONFIG"
|
||||
|
||||
elif [ "launchd" == "$my_system_launcher" ]; then
|
||||
|
||||
my_stopper="${real_sudo_cmde}launchctl unload $my_app_launchd_service"
|
||||
echo "Edit the config and restart, if desired:"
|
||||
echo ""
|
||||
echo " ${real_sudo_cmde}$my_edit $TELEBITD_CONFIG"
|
||||
echo " ${real_sudo_cmde}launchctl unload $my_app_launchd_service"
|
||||
echo " ${real_sudo_cmde}launchctl load -w $my_app_launchd_service"
|
||||
echo ""
|
||||
echo "Or disabled the service and start manually:"
|
||||
echo ""
|
||||
echo " ${real_sudo_cmde}launchctl unload -w $my_app_launchd_service"
|
||||
echo " $my_daemon --config $TELEBITD_CONFIG"
|
||||
|
||||
else
|
||||
|
||||
my_stopper="not started"
|
||||
echo ""
|
||||
echo "Run the service manually (we couldn't detect your system service to do that automatically):"
|
||||
echo ""
|
||||
echo " $my_daemon --config $TELEBITD_CONFIG"
|
||||
echo " $my_app --config $TELEBIT_CONFIG"
|
||||
|
||||
fi
|
|
@ -1,5 +0,0 @@
|
|||
#agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
|
||||
community_member: true # receive infrequent relevant updates
|
||||
telemetry: true # contribute to project telemetric data
|
||||
newsletter: false # contribute to project telemetric data
|
||||
ssh_auto: false # forward ssh-looking packets, from any connection, to port 22
|
|
@ -1,97 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
|
||||
module.exports = function (opts, fn) {
|
||||
// TODO make async version
|
||||
try {
|
||||
module.exports.sync(opts);
|
||||
} catch(e) {
|
||||
if (fn) { fn(e); }
|
||||
}
|
||||
|
||||
if (fn) { fn(null); }
|
||||
};
|
||||
module.exports.sync = function (opts) {
|
||||
var f = opts.file;
|
||||
var vars = opts.vars;
|
||||
var text = fs.readFileSync(f.tpl, 'utf8')
|
||||
.replace(/{TELEBIT_PATH}/g, vars.telebitPath || '{TELEBIT_PATH}')
|
||||
.replace(/{TELEBIT_NODE}/g, vars.telebitNode || '{TELEBIT_NODE}')
|
||||
.replace(/{NODE_PATH}/g, vars.nodePath || '{NODE_PATH}')
|
||||
.replace(/{NPM_CONFIG_PREFIX}/g, vars.npmConfigPrefix || '{NPM_CONFIG_PREFIX}')
|
||||
.replace(/{TELEBIT_NPM}/g, vars.telebitNpm || '{TELEBIT_NPM}')
|
||||
.replace(/{TELEBIT_BIN}/g, vars.telebitBin || '{TELEBIT_BIN}')
|
||||
.replace(/{TELEBITD_BIN}/g, vars.telebitdBin || '{TELEBITD_BIN}')
|
||||
.replace(/{TELEBIT_JS}/g, vars.telebitJs || '{TELEBIT_JS}')
|
||||
.replace(/{TELEBITD_JS}/g, vars.telebitdJs || '{TELEBITD_JS}')
|
||||
.replace(/{TELEBIT_USER}/g, vars.telebitUser || '{TELEBIT_USER}')
|
||||
.replace(/{TELEBIT_GROUP}/g, vars.telebitGroup || '{TELEBIT_GROUP}')
|
||||
.replace(/{TELEBIT_RW_DIRS}/g, vars.telebitRwDirs || '{TELEBIT_RW_DIRS}')
|
||||
.replace(/{TELEBIT_CONFIG}/g, vars.telebitConfig || '{TELEBIT_CONFIG}')
|
||||
.replace(/{TELEBITD_CONFIG}/g, vars.telebitdConfig || '{TELEBITD_CONFIG}')
|
||||
.replace(/{TELEBIT_LOG_DIR}/g, vars.TELEBIT_LOG_DIR || '{TELEBIT_LOG_DIR}')
|
||||
.replace(/{TELEBIT_SOCK_DIR}/g, vars.TELEBIT_LOG_DIR || '{TELEBIT_SOCK_DIR}')
|
||||
;
|
||||
fs.writeFileSync(f.launcher, text, 'utf8');
|
||||
if (f.executable && !/^win/i.test(os.platform())) {
|
||||
// TODO not sure if chmod works on windows
|
||||
fs.chmodSync(f.launcher, parseInt('755', 8));
|
||||
}
|
||||
};
|
||||
|
||||
function run() {
|
||||
var files = [
|
||||
{ tpl: (process.env.TELEBIT_SERVICE_TPL || path.join(__dirname, 'dist/etc/systemd/system/telebit.service.tpl'))
|
||||
, launcher: (process.env.TELEBIT_SERVICE || path.join(__dirname, 'dist/etc/systemd/system/telebit.service'))
|
||||
}
|
||||
, { tpl: (process.env.TELEBIT_USER_SERVICE_TPL || path.join(__dirname, 'dist/etc/skel/.config/systemd/user/telebit.service.tpl'))
|
||||
, launcher: (process.env.TELEBIT_USER_SERVICE || path.join(__dirname, 'dist/etc/skel/.config/systemd/user/telebit.service'))
|
||||
}
|
||||
, { tpl: (process.env.TELEBIT_PLIST_TPL || path.join(__dirname, 'dist/Library/LaunchDaemons/cloud.telebit.remote.plist.tpl'))
|
||||
, launcher: (process.env.TELEBIT_PLIST || path.join(__dirname, 'dist/Library/LaunchDaemons/cloud.telebit.remote.plist'))
|
||||
}
|
||||
, { tpl: (process.env.TELEBIT_USER_PLIST_TPL || path.join(__dirname, 'dist/etc/skel/Library/LaunchAgents/cloud.telebit.remote.plist.tpl'))
|
||||
, launcher: (process.env.TELEBIT_USER_PLIST || path.join(__dirname, 'dist/etc/skel/Library/LaunchAgents/cloud.telebit.remote.plist'))
|
||||
}
|
||||
];
|
||||
|
||||
files.forEach(function (f) {
|
||||
var telebitRoot = path.resolve(__dirname, '../..');
|
||||
var vars = {
|
||||
telebitPath: process.env.TELEBIT_PATH || telebitRoot
|
||||
, telebitNode: process.env.TELEBIT_NODE || process.argv[0] || path.resolve(telebitRoot, 'bin/node')
|
||||
, telebitBin: process.env.TELEBIT_BIN || path.resolve(telebitRoot, 'bin/telebit')
|
||||
, telebitdBin: process.env.TELEBITD_BIN || path.resolve(telebitRoot, 'bin/telebitd')
|
||||
, telebitJs: process.env.TELEBIT_JS || path.resolve(telebitRoot, 'bin/telebit.js')
|
||||
, telebitdJs: process.env.TELEBITD_JS || path.resolve(telebitRoot, 'bin/telebitd.js')
|
||||
, telebitRwDirs: [
|
||||
(process.env.TELEBIT_PATH || path.resolve(__dirname, '../..'))
|
||||
, path.join(os.homedir(), '.config/telebit')
|
||||
, path.join(os.homedir(), '.local/share/telebit')
|
||||
]
|
||||
, telebitUser: process.env.TELEBIT_USER || os.userInfo().username
|
||||
, telebitGroup: process.env.TELEBIT_GROUP || ('darwin' === os.platform() ? 'staff' : os.userInfo().username)
|
||||
, telebitConfig: process.env.TELEBIT_CONFIG || path.join(os.homedir(), '.config/telebit/telebit.yml')
|
||||
, telebitdConfig: process.env.TELEBITD_CONFIG || path.join(os.homedir(), '.config/telebit/telebitd.yml')
|
||||
, TELEBIT_LOG_DIR: process.env.TELEBIT_LOG_DIR || path.join(os.homedir(), '.local/share/telebit/var/log')
|
||||
};
|
||||
vars.telebitNpm = process.env.TELEBIT_NPM || path.resolve(vars.telebitNode, '../npm');
|
||||
vars.nodePath = process.env.NODE_PATH || path.resolve(vars.telebitNode, '../lib/node_modules');
|
||||
vars.npmConfigPrefix = process.env.NPM_CONFIG_PREFIX || path.resolve(vars.telebitNode, '..');
|
||||
if (-1 === vars.telebitRwDirs.indexOf(vars.npmConfigPrefix)) {
|
||||
vars.telebitRwDirs.push(vars.npmConfigPrefix);
|
||||
}
|
||||
vars.telebitRwDirs = vars.telebitRwDirs.join(' ');
|
||||
module.exports({
|
||||
file: f
|
||||
, vars: vars
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
run();
|
||||
}
|
|
@ -1,16 +1,10 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
var PromiseA;
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
} catch(e) {
|
||||
PromiseA = global.Promise;
|
||||
}
|
||||
var WebSocket = require('ws');
|
||||
var PromiseA = require('bluebird');
|
||||
var sni = require('sni');
|
||||
var Packer = require('proxy-packer');
|
||||
var os = require('os');
|
||||
var Packer = require('tunnel-packer');
|
||||
|
||||
function timeoutPromise(duration) {
|
||||
return new PromiseA(function (resolve) {
|
||||
|
@ -18,41 +12,30 @@ function timeoutPromise(duration) {
|
|||
});
|
||||
}
|
||||
|
||||
function _connect(state) {
|
||||
function run(copts) {
|
||||
// jshint latedef:false
|
||||
var defaultHttpTimeout = (2 * 60);
|
||||
var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
|
||||
var pongTimeout = state.pongTimeout || 10*1000;
|
||||
var activityTimeout = copts.activityTimeout || (2*60 - 5)*1000;
|
||||
var pongTimeout = copts.pongTimeout || 10*1000;
|
||||
// Allow the tunnel client to be created with no token. This will prevent the connection from
|
||||
// being established initialy and allows the caller to use `.append` for the first token so
|
||||
// they can get a promise that will provide feedback about invalid tokens.
|
||||
var tokens = [];
|
||||
var auth;
|
||||
if(!state.sortingHat) {
|
||||
state.sortingHat = "./sorting-hat.js";
|
||||
}
|
||||
if (state.token) {
|
||||
if ('undefined' === state.token) {
|
||||
throw new Error("passed string 'undefined' as token");
|
||||
}
|
||||
tokens.push(state.token);
|
||||
if (copts.token) {
|
||||
tokens.push(copts.token);
|
||||
}
|
||||
|
||||
var wstunneler;
|
||||
var authenticated = false;
|
||||
var authsent = false;
|
||||
var initialConnect = true;
|
||||
|
||||
var localclients = {};
|
||||
var pausedClients = [];
|
||||
var clientHandlers = {
|
||||
add: function (conn, cid, tun) {
|
||||
add: function (conn, cid, opts, servername) {
|
||||
localclients[cid] = conn;
|
||||
console.info("[connect] new client '" + cid + "' for '" + tun.name + ":" + tun.serviceport + "' "
|
||||
+ "(" + clientHandlers.count() + " clients)");
|
||||
console.info("[connect] new client '" + cid + "' for '" + servername + "' (" + clientHandlers.count() + " clients)");
|
||||
|
||||
conn.tunnelCid = cid;
|
||||
conn.tunnelRead = tun.data.byteLength;
|
||||
conn.tunnelRead = opts.data.byteLength;
|
||||
conn.tunnelWritten = 0;
|
||||
|
||||
conn.on('data', function onLocalData(chunk) {
|
||||
|
@ -67,7 +50,7 @@ function _connect(state) {
|
|||
// down the data we are getting to send over. We also want to pause all active connections
|
||||
// if any connections are paused to make things more fair so one connection doesn't get
|
||||
// stuff waiting for all other connections to finish because it tried writing near the border.
|
||||
var bufSize = wsHandlers.sendMessage(Packer.pack(tun, chunk));
|
||||
var bufSize = wsHandlers.sendMessage(Packer.pack(opts, chunk));
|
||||
if (pausedClients.length || bufSize > 1024*1024) {
|
||||
// console.log('[onLocalData] paused connection', cid, 'to allow websocket to catch up');
|
||||
conn.pause();
|
||||
|
@ -80,14 +63,14 @@ function _connect(state) {
|
|||
console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon");
|
||||
conn.tunnelClosing = true;
|
||||
if (!sentEnd) {
|
||||
wsHandlers.sendMessage(Packer.pack(tun, null, 'end'));
|
||||
wsHandlers.sendMessage(Packer.pack(opts, null, 'end'));
|
||||
sentEnd = true;
|
||||
}
|
||||
});
|
||||
conn.on('error', function onLocalError(err) {
|
||||
console.info("[onLocalError] connection '" + cid + "' errored:", err);
|
||||
if (!sentEnd) {
|
||||
wsHandlers.sendMessage(Packer.pack(tun, {message: err.message, code: err.code}, 'error'));
|
||||
wsHandlers.sendMessage(Packer.pack(opts, {message: err.message, code: err.code}, 'error'));
|
||||
sentEnd = true;
|
||||
}
|
||||
});
|
||||
|
@ -95,7 +78,7 @@ function _connect(state) {
|
|||
delete localclients[cid];
|
||||
console.log('[onLocalClose] closed "' + cid + '" read:'+conn.tunnelRead+', wrote:'+conn.tunnelWritten+' (' + clientHandlers.count() + ' clients)');
|
||||
if (!sentEnd) {
|
||||
wsHandlers.sendMessage(Packer.pack(tun, null, hadErr && 'error' || 'end'));
|
||||
wsHandlers.sendMessage(Packer.pack(opts, null, hadErr && 'error' || 'end'));
|
||||
sentEnd = true;
|
||||
}
|
||||
});
|
||||
|
@ -186,12 +169,11 @@ function _connect(state) {
|
|||
function sendCommand(name) {
|
||||
var id = Math.ceil(1e9 * Math.random());
|
||||
var cmd = [id, name].concat(Array.prototype.slice.call(arguments, 1));
|
||||
if (state.debug) { console.log('[DEBUG] command sending', cmd); }
|
||||
|
||||
wsHandlers.sendMessage(Packer.pack(null, cmd, 'control'));
|
||||
setTimeout(function () {
|
||||
if (pendingCommands[id]) {
|
||||
console.warn('command', name, id, 'timed out');
|
||||
console.warn('command', id, 'timed out');
|
||||
pendingCommands[id]({
|
||||
message: 'response not received in time'
|
||||
, code: 'E_TIMEOUT'
|
||||
|
@ -212,13 +194,7 @@ function _connect(state) {
|
|||
}
|
||||
|
||||
function sendAllTokens() {
|
||||
if (auth) {
|
||||
authsent = true;
|
||||
sendCommand('auth', auth).catch(function (err) { console.error('1', err); });
|
||||
}
|
||||
tokens.forEach(function (jwtoken) {
|
||||
if (state.debug) { console.log('[DEBUG] send token'); }
|
||||
authsent = true;
|
||||
sendCommand('add_token', jwtoken)
|
||||
.catch(function (err) {
|
||||
console.error('failed re-adding token', jwtoken, 'after reconnect', err);
|
||||
|
@ -229,11 +205,6 @@ function _connect(state) {
|
|||
});
|
||||
}
|
||||
|
||||
function noHandler(cmd) {
|
||||
console.warn("[telebit] state.handlers['" + cmd[1] + "'] not set");
|
||||
console.warn(cmd[2]);
|
||||
}
|
||||
|
||||
var connCallback;
|
||||
|
||||
var packerHandlers = {
|
||||
|
@ -266,7 +237,8 @@ function _connect(state) {
|
|||
}
|
||||
|
||||
if (cmd[1] === 'hello') {
|
||||
if (state.debug) { console.log('[DEBUG] hello received'); }
|
||||
// We only get the 'hello' event after the token has been validated
|
||||
authenticated = true;
|
||||
sendAllTokens();
|
||||
if (connCallback) {
|
||||
connCallback();
|
||||
|
@ -274,58 +246,90 @@ function _connect(state) {
|
|||
// TODO: handle the versions and commands provided by 'hello' - isn't super important
|
||||
// yet since there is only one version and set of commands.
|
||||
err = null;
|
||||
} else if (cmd[1] === 'grant') {
|
||||
authenticated = true;
|
||||
if (state.handlers[cmd[1]]) {
|
||||
state.handlers[cmd[1]](cmd[2]);
|
||||
} else {
|
||||
noHandler(cmd);
|
||||
}
|
||||
return;
|
||||
} else if (cmd[1] === 'access_token') {
|
||||
authenticated = true;
|
||||
if (state.handlers[cmd[1]]) {
|
||||
state.handlers[cmd[1]](cmd[2]);
|
||||
} else {
|
||||
noHandler(cmd);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
err = { message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' };
|
||||
}
|
||||
|
||||
wsHandlers.sendMessage(Packer.pack(null, [-cmd[0], err], 'control'));
|
||||
}
|
||||
|
||||
, onmessage: function (tun) {
|
||||
var cid = tun._id = Packer.addrToId(tun);
|
||||
, onmessage: function (opts) {
|
||||
var net = copts.net || require('net');
|
||||
var cid = Packer.addrToId(opts);
|
||||
var service = opts.service.toLowerCase();
|
||||
var portList = copts.services[service];
|
||||
var servername;
|
||||
var port;
|
||||
var str;
|
||||
var m;
|
||||
|
||||
if ('http' === tun.service) {
|
||||
str = tun.data.toString();
|
||||
if (clientHandlers.write(cid, opts)) {
|
||||
return;
|
||||
}
|
||||
if (!portList) {
|
||||
packerHandlers._onConnectError(cid, opts, new Error("unsupported service '" + service + "'"));
|
||||
return;
|
||||
}
|
||||
|
||||
if ('http' === service) {
|
||||
str = opts.data.toString();
|
||||
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
||||
tun._name = tun._hostname = (m && m[1].toLowerCase() || '').split(':')[0];
|
||||
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
||||
}
|
||||
else if ('https' === tun.service || 'tls' === tun.service) {
|
||||
tun._name = tun._servername = sni(tun.data);
|
||||
} else {
|
||||
tun._name = '';
|
||||
else if ('https' === service) {
|
||||
servername = sni(opts.data);
|
||||
}
|
||||
else {
|
||||
servername = '*';
|
||||
}
|
||||
|
||||
if (clientHandlers.write(cid, tun)) { return; }
|
||||
if (!servername) {
|
||||
//console.warn(opts.data.toString());
|
||||
packerHandlers._onConnectError(cid, opts, new Error("missing servername for '" + cid + "' " + opts.data.byteLength));
|
||||
return;
|
||||
}
|
||||
|
||||
wstunneler.pause();
|
||||
require(state.sortingHat).assign(state, tun, function (err, conn) {
|
||||
if (err) {
|
||||
err.message = err.message.replace(/:tun_id/, tun._id);
|
||||
packerHandlers._onConnectError(cid, tun, err);
|
||||
return;
|
||||
port = portList[servername];
|
||||
if (!port) {
|
||||
// Check for any wildcard domains, sorted longest to shortest so the one with the
|
||||
// biggest natural match will be found first.
|
||||
Object.keys(portList).filter(function (pattern) {
|
||||
return pattern[0] === '*' && pattern.length > 1;
|
||||
}).sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
}).some(function (pattern) {
|
||||
var subPiece = pattern.slice(1);
|
||||
if (subPiece === servername.slice(-subPiece.length)) {
|
||||
port = portList[pattern];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!port) {
|
||||
port = portList['*'];
|
||||
}
|
||||
|
||||
var createOpts = {
|
||||
port: port
|
||||
, host: '127.0.0.1'
|
||||
|
||||
, servername: servername
|
||||
, data: opts.data
|
||||
, remoteFamily: opts.family
|
||||
, remoteAddress: opts.address
|
||||
, remotePort: opts.port
|
||||
};
|
||||
var conn = net.createConnection(createOpts, function () {
|
||||
// this will happen before 'data' or 'readable' is triggered
|
||||
// We use the data from the createOpts object so that the createConnection function has
|
||||
// the oppurtunity of removing/changing it if it wants/needs to handle it differently.
|
||||
if (createOpts.data) {
|
||||
conn.write(createOpts.data);
|
||||
}
|
||||
clientHandlers.add(conn, cid, tun);
|
||||
if (tun.data) { conn.write(tun.data); }
|
||||
wstunneler.resume();
|
||||
});
|
||||
|
||||
clientHandlers.add(conn, cid, opts, servername);
|
||||
}
|
||||
|
||||
, onpause: function (opts) {
|
||||
|
@ -411,9 +415,8 @@ function _connect(state) {
|
|||
}
|
||||
|
||||
, onOpen: function () {
|
||||
console.info("[open] connected to '" + (state.wss || state.relay) + "'");
|
||||
console.info("[open] connected to '" + copts.stunneld + "'");
|
||||
wsHandlers.refreshTimeout();
|
||||
|
||||
timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout);
|
||||
|
||||
wstunneler._socket.on('drain', function () {
|
||||
|
@ -435,17 +438,10 @@ function _connect(state) {
|
|||
|
||||
pausedClients.length = 0;
|
||||
});
|
||||
|
||||
//Call either Open or Reconnect handlers.
|
||||
if(state.handlers.onOpen && initialConnect) {
|
||||
state.handlers.onOpen();
|
||||
} else if (state.handlers.onReconnect && !initialConnect) {
|
||||
state.handlers.onReconnect();
|
||||
}
|
||||
initialConnect = false;
|
||||
}
|
||||
|
||||
, onClose: function () {
|
||||
console.log('ON CLOSE');
|
||||
clearTimeout(timeoutId);
|
||||
wstunneler = null;
|
||||
clientHandlers.closeAll();
|
||||
|
@ -460,35 +456,16 @@ function _connect(state) {
|
|||
}
|
||||
|
||||
if (!authenticated) {
|
||||
if(state.handlers.onError) {
|
||||
var err = new Error('Failed to connect on first attempt... check authentication');
|
||||
state.handlers.onError(err);
|
||||
}
|
||||
if(state.handlers.onClose) {
|
||||
state.handlers.onClose();
|
||||
}
|
||||
console.info('[close] failed on first attempt... check authentication.');
|
||||
timeoutId = null;
|
||||
}
|
||||
else if (tokens.length) {
|
||||
if(state.handlers.onDisconnect) {
|
||||
state.handlers.onDisconnect();
|
||||
}
|
||||
console.info('[retry] disconnected and waiting...');
|
||||
timeoutId = setTimeout(connect, 5000);
|
||||
} else {
|
||||
if(state.handlers.onClose) {
|
||||
state.handlers.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, onError: function (err) {
|
||||
if ('ENOTFOUND' === err.code) {
|
||||
// DNS issue, probably network is disconnected
|
||||
timeoutId = setTimeout(connect, 90 * 1000);
|
||||
return;
|
||||
}
|
||||
console.error("[tunnel error] " + err.message);
|
||||
console.error(err);
|
||||
if (connCallback) {
|
||||
|
@ -513,33 +490,19 @@ function _connect(state) {
|
|||
};
|
||||
|
||||
function connect() {
|
||||
if (!tokens.length) {
|
||||
return;
|
||||
}
|
||||
if (wstunneler) {
|
||||
console.warn('attempted to connect with connection already active');
|
||||
return;
|
||||
}
|
||||
if (!tokens.length) {
|
||||
if (state.config.email) {
|
||||
auth = {
|
||||
subject: state.config.email
|
||||
, subject_scheme: 'mailto'
|
||||
// TODO create domains list earlier
|
||||
, scope: Object.keys(state.config.servernames || {}).join(',')
|
||||
, otp: state.otp
|
||||
, hostname: os.hostname()
|
||||
// Used for User-Agent
|
||||
, os_type: os.type()
|
||||
, os_platform: os.platform()
|
||||
, os_release: os.release()
|
||||
, os_arch: os.arch()
|
||||
};
|
||||
}
|
||||
}
|
||||
timeoutId = null;
|
||||
var machine = Packer.create(packerHandlers);
|
||||
var machine = require('tunnel-packer').create(packerHandlers);
|
||||
|
||||
console.info("[connect] '" + (state.wss || state.relay) + "'");
|
||||
var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
|
||||
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
|
||||
console.info("[connect] '" + copts.stunneld + "'");
|
||||
var tunnelUrl = copts.stunneld.replace(/\/$/, '') + '/?access_token=' + tokens[0];
|
||||
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !copts.insecure });
|
||||
wstunneler.on('open', wsHandlers.onOpen);
|
||||
wstunneler.on('close', wsHandlers.onClose);
|
||||
wstunneler.on('error', wsHandlers.onError);
|
||||
|
@ -560,7 +523,7 @@ function _connect(state) {
|
|||
|
||||
var connPromise;
|
||||
return {
|
||||
end: function(cb) {
|
||||
end: function() {
|
||||
tokens.length = 0;
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
|
@ -569,7 +532,7 @@ function _connect(state) {
|
|||
|
||||
if (wstunneler) {
|
||||
try {
|
||||
wstunneler.close(cb);
|
||||
wstunneler.close();
|
||||
} catch(e) {
|
||||
console.error("[error] wstunneler.close()");
|
||||
console.error(e);
|
||||
|
@ -577,12 +540,6 @@ function _connect(state) {
|
|||
}
|
||||
}
|
||||
, append: function (token) {
|
||||
if (!token) {
|
||||
throw new Error("attempted to append empty token");
|
||||
}
|
||||
if ('undefined' === token) {
|
||||
throw new Error("attempted to append token as the string 'undefined'");
|
||||
}
|
||||
if (tokens.indexOf(token) >= 0) {
|
||||
return PromiseA.resolve();
|
||||
}
|
||||
|
@ -656,7 +613,7 @@ function _connect(state) {
|
|||
};
|
||||
}
|
||||
|
||||
module.exports.connect = _connect;
|
||||
module.exports.createConnection = _connect;
|
||||
module.exports.connect = run;
|
||||
module.exports.createConnection = run;
|
||||
|
||||
}());
|
Loading…
Reference in New Issue