Compare commits

..

324 Commits

Author SHA1 Message Date
AJ ONeal 5908871633 use 'who am i' rather than 'logname' 2018-07-26 16:46:57 -06:00
AJ ONeal 214096e216 fix null pipe 2018-07-26 16:40:06 -06:00
AJ ONeal aaa11c4c87 ignore bad logname on some linuxes 2018-07-26 16:35:14 -06:00
AJ ONeal 515d74a1ea fix #29 by uri encoding and decoding json 2018-07-26 11:26:34 -06:00
AJ ONeal 28cef77806 v0.19.26: [linux] ignore bad stderr 2018-07-21 04:42:44 -06:00
AJ ONeal c481d759d6 v0.19.25: bump for the sake of dependencies with windows bugfixes 2018-07-12 01:16:01 -06:00
AJ ONeal 4b22e7675e v0.19.24: bugfix windows https://github.com/nodejs/node/issues/21771 2018-07-11 18:11:37 -06:00
AJ ONeal 7f3df579ac v0.19.23 2018-07-07 20:19:41 -06:00
AJ ONeal 5bc9a58451 conditional logging 2018-07-07 20:19:03 -06:00
AJ ONeal 7dc24314c8 use patched urequest 2018-07-07 19:58:06 -06:00
AJ ONeal 4acf294caa show version as needed 2018-07-07 19:08:14 -06:00
AJ ONeal 55ed40fce7 show set variables 2018-07-07 19:06:17 -06:00
AJ ONeal ac15f4d30c use node v10.6 2018-07-07 18:53:01 -06:00
AJ ONeal da31c0154f lots of debugging 2018-07-07 18:46:45 -06:00
AJ ONeal 96424aad4b v0.19.22: only interpret strings with / or \ as paths 2018-07-05 22:33:05 -06:00
AJ ONeal 9c7a6c6eec v0.19.21: Fix #27 2018-07-05 22:13:56 -06:00
AJ ONeal 959ed59b2f use console.info to denote intentional output 2018-07-03 05:07:42 -06:00
AJ ONeal ebe03df035 document my unverifiable woes 2018-07-03 04:53:24 -06:00
AJ ONeal 182045315b v0.19.20: winfixes 2018-07-03 04:40:21 -06:00
AJ ONeal 8f7ab08a99 don't define host and port, even as null 2018-07-03 04:37:41 -06:00
AJ ONeal 219166670b v0.19.19: winfixes 2018-07-03 04:13:31 -06:00
AJ ONeal eec7bab97a minor cleanup and longer wait for windows pipe 2018-07-03 04:12:53 -06:00
AJ ONeal 3a71298183 v0.19.18: correct usage of windows pipe and retry connection on initial network failure 2018-07-03 03:43:47 -06:00
AJ ONeal 82763460ef retry after bad network conn 2018-07-03 03:42:17 -06:00
AJ ONeal b9fae99ddc winfix: better pipe support 2018-07-02 20:30:18 -06:00
AJ ONeal d52b4e7594 v0.19.17: include usr/share for npm install 2018-07-02 12:03:10 -06:00
AJ ONeal 50cae93370 v0.19.16 2018-07-01 01:36:04 -06:00
AJ ONeal 5723b368db fix error condition 2018-07-01 01:35:45 -06:00
AJ ONeal cb322b4b1a v0.19.15 2018-07-01 01:18:30 -06:00
AJ ONeal f0fa9c8615 send files over tcp 2018-07-01 01:18:10 -06:00
AJ ONeal 9a1e4e3b06 v0.19.14 2018-07-01 00:53:39 -06:00
AJ ONeal d3d488c1d3 handle files, sites, and directories 2018-07-01 00:53:26 -06:00
AJ ONeal e5076a3917 v0.19.13 2018-06-30 20:46:06 -06:00
AJ ONeal ba0afc1bff pass acceptd domains, not fail them, duh 2018-06-30 20:45:42 -06:00
AJ ONeal 7214890e5b v0.19.12 2018-06-30 20:24:56 -06:00
AJ ONeal e578aadab0 v0.19.11 2018-06-30 20:23:50 -06:00
AJ ONeal ecc97558d8 v0.19.10 2018-06-30 20:16:11 -06:00
AJ ONeal 5b4dbbd77a show https:// prefix 2018-06-30 20:15:52 -06:00
AJ ONeal 649414403e better output before install exit 2018-06-30 20:14:38 -06:00
AJ ONeal c1e09ce441 v0.19.9 2018-06-30 19:52:52 -06:00
AJ ONeal 9bcd78bd35 update docs 2018-06-30 19:52:34 -06:00
AJ ONeal e4ca414ab7 v0.19.8 2018-06-30 19:34:31 -06:00
AJ ONeal fc0054bbf1 make the output bearable 2018-06-30 19:34:15 -06:00
AJ ONeal a015db2d16 silence! 2018-06-30 18:27:47 -06:00
AJ ONeal a3cc4a7f78 silence and bugfix too little time to start 2018-06-30 18:25:54 -06:00
AJ ONeal f18caced93 v0.19.7 2018-06-30 18:18:25 -06:00
AJ ONeal 9e61caaa6c bugfix missing relay on init 2018-06-30 18:18:03 -06:00
AJ ONeal 27a85a3e73 v0.19.6 2018-06-30 17:56:43 -06:00
AJ ONeal b5136a9f08 #11 wildcard support 2018-06-30 17:56:10 -06:00
AJ ONeal 99589fa2ad v0.19.5 2018-06-30 16:18:40 -06:00
AJ ONeal 32e148961d tested normal and error conditions for a few different things 2018-06-30 16:18:22 -06:00
AJ ONeal abf73259a6 v0.19.4 2018-06-30 13:20:17 -06:00
AJ ONeal 7a36b1af8a bugfix: show success, not just error 2018-06-30 13:20:00 -06:00
AJ ONeal 90fda2ac98 v0.19.3 2018-06-29 17:31:51 -06:00
AJ ONeal b2b9094d60 error message when localhost app isn't running 2018-06-29 17:31:34 -06:00
AJ ONeal 1bb6a82f77 v0.19.2 2018-06-29 16:16:25 -06:00
AJ ONeal 9c5aeda776 don't kill process on cli commands 2018-06-29 16:16:03 -06:00
AJ ONeal 85f07fd021 v0.19.1 2018-06-29 15:54:18 -06:00
AJ ONeal a928decbd8 renable parseCli 2018-06-29 15:53:55 -06:00
AJ ONeal 59baac2d2e prepare to use ports instead of pipes, if needed 2018-06-29 15:18:32 -06:00
AJ ONeal 8c3a89c25b v0.19.0 2018-06-29 14:52:33 -06:00
AJ ONeal 5584638907 working: manage token with client 2018-06-29 14:51:56 -06:00
AJ ONeal fce8e366c6 save config on upgrade token 2018-06-29 04:33:22 -06:00
AJ ONeal 9441bf7f8e WIP get preauth clientside and realauth daemonside 2018-06-29 04:15:23 -06:00
AJ ONeal 02a89b52a4 server will wait for token 2018-06-28 20:35:58 -06:00
AJ ONeal 2456228ae5 login/create account 2018-06-28 17:19:49 -06:00
AJ ONeal 7929512fe5 silence enable success output that happens on stderr 2018-06-28 17:14:05 -06:00
AJ ONeal cf063ec6e2 revert to not as good progress 2018-06-28 16:17:41 -06:00
AJ ONeal 998c198f0c try better progress 2018-06-28 16:12:51 -06:00
AJ ONeal 9974a8ab47 more awesome progress 2018-06-28 15:48:12 -06:00
AJ ONeal 3f319c5e9e remove some extra lines 2018-06-28 15:43:34 -06:00
AJ ONeal fcc6c6c5b5 Quiet down and stop speaking nonsense! 2018-06-28 15:37:31 -06:00
AJ ONeal 999e8dfb5c SILENCE! 2018-06-28 15:26:07 -06:00
AJ ONeal 92c1180d0f fix npm progress pid 2018-06-28 15:23:02 -06:00
AJ ONeal cc6f6fc537 fix npm progress pid 2018-06-28 15:21:33 -06:00
AJ ONeal cbf8910e3e progress bar for npm install 2018-06-28 15:18:19 -06:00
AJ ONeal 330f28ad07 make more conditional 2018-06-28 15:09:24 -06:00
AJ ONeal 2ae4b5a25a SILENCE you fools! 2018-06-28 14:47:32 -06:00
AJ ONeal 0c924753c3 don't delete the runner, duh 2018-06-28 13:48:24 -06:00
AJ ONeal a510cbe74e v0.18.9 2018-06-28 13:36:16 -06:00
AJ ONeal 69e28819d5 create log and sock dirs 2018-06-28 13:31:39 -06:00
AJ ONeal 39e6c98bed update tmpdir 2018-06-28 13:15:06 -06:00
AJ ONeal 4d6f51f204 update links and executable perms 2018-06-28 13:01:19 -06:00
AJ ONeal 38a4a1f0ef hard remove existing links 2018-06-28 12:48:20 -06:00
AJ ONeal 775077da28 v0.18.8 2018-06-28 12:38:26 -06:00
AJ ONeal 724460bb9e bugfix: always create sock dir 2018-06-28 12:37:57 -06:00
AJ ONeal 18b2584728 make 'press any key' last 2018-06-28 03:46:54 -06:00
AJ ONeal 28a66a9bd7 v0.18.7 2018-06-28 03:38:42 -06:00
AJ ONeal 2892316e25 read, not write, duh 2018-06-28 03:31:31 -06:00
AJ ONeal ac1e46da1f pass flags directly, not as object property 2018-06-28 03:30:29 -06:00
AJ ONeal 7301ec1125 correction: first argument must be pathname 2018-06-28 03:29:05 -06:00
AJ ONeal c1917f010b bugfix ignored path must still be a string 2018-06-28 03:27:34 -06:00
AJ ONeal 28eb15d2e4 typo fix mdir => mkdir 2018-06-28 03:25:36 -06:00
AJ ONeal a8d5d91110 v0.18.6 2018-06-28 03:12:45 -06:00
AJ ONeal 18316b805e bugfixes windows start, node daemon error 2018-06-28 03:12:13 -06:00
AJ ONeal 6f935f6735 v0.18.5 2018-06-28 03:03:57 -06:00
AJ ONeal 9c83da19a4 remove deps 2018-06-28 03:02:14 -06:00
AJ ONeal fa7e33d984 v0.18.4 2018-06-28 02:37:53 -06:00
AJ ONeal 32b87f2486 refactor otp assignment slightly 2018-06-28 02:36:54 -06:00
AJ ONeal 58f06a7bf3 v0.18.3 2018-06-28 02:22:15 -06:00
AJ ONeal 75c971c598 good default acme dir 2018-06-28 02:20:51 -06:00
AJ ONeal 9ae2bcc997 note npx usage 2018-06-28 02:07:19 -06:00
AJ ONeal e67555cf52 v0.18.2 2018-06-28 02:02:02 -06:00
AJ ONeal d427280018 bugfix user systemd 2018-06-28 02:01:43 -06:00
AJ ONeal ce00a9c721 bugfixes and comments 2018-06-28 01:57:57 -06:00
AJ ONeal eb27cde8c0 bugfixes and cleanup 2018-06-28 01:53:03 -06:00
AJ ONeal 4b168225f0 update template vars 2018-06-28 01:18:02 -06:00
AJ ONeal b1b5e32525 correctly pass error on failure to launch 2018-06-28 00:44:41 -06:00
AJ ONeal ca933704e1 v0.18.1 2018-06-27 23:09:43 -06:00
AJ ONeal b3b44cdd33 update some templates and fix some bugs 2018-06-27 22:58:43 -06:00
AJ ONeal 83f824a08b v0.18.0 2018-06-27 21:36:30 -06:00
AJ ONeal 172eaea877 update to npm launcher 2018-06-27 21:35:14 -06:00
AJ ONeal 5eb4cdefb7 node-based install launcher tested on macOS 2018-06-27 04:24:56 -06:00
AJ ONeal d85682ff7f WIP make installable from npm 2018-06-27 03:21:44 -06:00
AJ ONeal e8b3ae59c8 omit limits for now 2018-06-26 23:47:06 -06:00
AJ ONeal c77f8b6f15 add option O_NOCTTY to prevent terminal from hanging up 2018-06-26 19:11:46 -06:00
AJ ONeal 7406fad9e3 don't page systemctl status output 2018-06-26 01:24:12 -06:00
AJ ONeal b89c07276f correct .config paths 2018-06-26 01:20:54 -06:00
AJ ONeal 800a3b4abc check status 2018-06-26 01:12:53 -06:00
AJ ONeal 1b1082910e remove systemd options which are not valid for user, remove pre-templated files 2018-06-26 01:02:48 -06:00
AJ ONeal 107f021b13 remove user systemd tmp dir services 2018-06-26 00:29:53 -06:00
AJ ONeal 489c953d0b note su issues 2018-06-26 00:26:58 -06:00
AJ ONeal c0f592b78c use $HOME, not ~ 2018-06-26 00:13:12 -06:00
AJ ONeal d6780b7295 hide node installer info, mkdir for user systemd 2018-06-25 23:58:46 -06:00
AJ ONeal 55ccf9347d show node install info 2018-06-25 23:51:46 -06:00
AJ ONeal cea6738319 add preliminary user systemd support 2018-06-25 23:37:51 -06:00
AJ ONeal b22018191d workaround dev/tty bugs - tell user to hit any key 2018-06-24 03:01:06 -06:00
AJ ONeal 68ed91a5c9 workaround dev/tty bugs 2018-06-24 02:53:29 -06:00
AJ ONeal c55a348e4a echo typo fix 2018-06-24 02:45:32 -06:00
AJ ONeal 2e4422e5b8 pull service file from skel 2018-06-24 02:44:24 -06:00
AJ ONeal 83d4df0762 no sudo on osx 2018-06-24 02:36:09 -06:00
AJ ONeal fa29aa3faf proper TELEBIT_CONFIG vars 2018-06-24 02:16:04 -06:00
AJ ONeal 3e9620ee90 end std input from tty 2018-06-24 01:19:31 -06:00
AJ ONeal b2288e27c0 template unprivileged launcher 2018-06-24 01:18:59 -06:00
AJ ONeal 6137b1c0a8 add unprivileged LaunchAgent 2018-06-24 01:17:36 -06:00
AJ ONeal dd3b6e3512 fix question set, add final callback 2018-06-24 00:34:24 -06:00
AJ ONeal ae5313f39b use different variables for different scopes, duh 2018-06-23 23:37:49 -06:00
AJ ONeal 9968aad7a6 turn on debugging for a minute 2018-06-23 23:19:11 -06:00
AJ ONeal 39776e2d63 template system launcher, correct user and group 2018-06-23 19:19:12 -06:00
AJ ONeal 054c6a334a template launcher in node 2018-06-23 18:50:13 -06:00
AJ ONeal 76ae6cc501 mktemp -d first 2018-06-23 18:35:33 -06:00
AJ ONeal be13f167bd fix unbound var 2018-06-23 17:39:58 -06:00
AJ ONeal 472afaa2e7 avoid sudo 2018-06-23 17:38:57 -06:00
AJ ONeal fce06087c8 ask fewer questions 2018-06-23 16:02:08 -06:00
AJ ONeal 3e4b25b1f6 send error if response is weird 2018-06-22 23:55:18 -06:00
AJ ONeal 0a14bbd84c comments 2018-06-22 23:52:47 -06:00
AJ ONeal 6f88490abe ws 2018-06-21 15:01:31 -06:00
AJ ONeal c79e57a335 v0.17.0 2018-06-21 19:31:12 +00:00
AJ ONeal 32ad1103bc list stuff 2018-06-21 19:28:20 +00:00
AJ ONeal bcdd3cd951 don't show pairing when pairing is unavailable 2018-06-21 19:24:09 +00:00
AJ ONeal 5c28bff26b support older servers 2018-06-21 18:59:56 +00:00
AJ ONeal 5491b8d91d allow older relay versions, with warning 2018-06-21 18:56:36 +00:00
AJ ONeal 871e0d332a backward compat bugfix 2018-06-21 11:20:11 +00:00
AJ ONeal e2adc7aac4 add gif to ping 2018-06-21 11:04:01 +00:00
AJ ONeal fb1fafeb85 library-ize the token procedure a little bit 2018-06-21 11:01:16 +00:00
AJ ONeal 99b891fd99 handle pairing request via API 2018-06-21 06:10:49 +00:00
AJ ONeal a763ead434 Merge branch 'v1' of ssh://git.coolaj86.com:22042/coolaj86/telebit.js into v1 2018-06-20 09:07:42 +00:00
AJ ONeal e4640a8a00 getting closer to accounts via api 2018-06-20 09:07:35 +00:00
John Shaver 9dccf5b022 Added support for onOpen/onClose/onDisconnect/onReconnect handlers 2018-06-18 11:28:02 -07:00
John Shaver 7d1c553149 Added open/close/error/disconenct/reconenct handlers to remote. 2018-06-18 11:28:02 -07:00
John Shaver 00c973e728 Moved default sorting-hat value into remote.js 2018-06-18 11:28:02 -07:00
AJ ONeal 58ed4baff1 typo and bugfixes 2018-06-16 01:47:32 +00:00
AJ ONeal 8fbd49f0e6 WIP: use API to find tunnel 2018-06-16 01:11:02 +00:00
AJ ONeal 13326a89a6 ws 2018-06-15 16:48:39 -06:00
AJ ONeal b4a2108942 check standard apis 2018-06-15 22:45:42 +00:00
AJ ONeal 130219a758 ignore node installation stuff 2018-06-15 15:20:41 -06:00
AJ ONeal a135d4dd94 rename greenlock config obj 2018-06-14 20:18:42 -06:00
AJ ONeal cfbaf3621e remove accidental leading slash 2018-06-14 18:45:47 -06:00
AJ ONeal 05dd6d90e6 sign token with ports 2018-06-14 17:05:26 -06:00
AJ ONeal 9ea353f9a2 ignore vim swap files 2018-06-14 16:15:07 -06:00
AJ ONeal 0c2c97abb3 v0.16.1 2018-06-14 15:37:32 -06:00
AJ ONeal 4bfc16f064 split servernames and ports strings 2018-06-14 15:35:14 -06:00
AJ ONeal 2ffdc760a3 leave servernames and ports as arrays 2018-06-14 15:30:19 -06:00
AJ ONeal f32e9e7020 v0.16.0 2018-06-14 05:36:12 -06:00
AJ ONeal fdde0067eb version match tested working 2018-06-14 05:35:39 -06:00
AJ ONeal b6c6b81e73 preliminary updater hook 2018-06-14 05:24:28 -06:00
AJ ONeal e5b4ba6c84 and the other working node version 2018-06-14 03:37:22 -06:00
AJ ONeal 078fc284cd note the single working node version... 2018-06-14 03:05:57 -06:00
AJ ONeal 5e2aaee2b4 v0.15.4 2018-06-14 02:57:39 -06:00
AJ ONeal b29d5dcdb7 display 'telebit remote' and 'telebit daemon' 2018-06-14 02:42:41 -06:00
AJ ONeal 918437acbe whitespace 2018-06-14 02:40:52 -06:00
AJ ONeal 2045b054f8 print socket location 2018-06-14 02:39:34 -06:00
AJ ONeal a98bd306f3 log when using default pipe 2018-06-14 01:26:32 -06:00
AJ ONeal 8980e5a785 v0.15.3 2018-06-14 01:07:21 -06:00
AJ ONeal 6f98c7305a add telebitd as one of the bins 2018-06-14 01:04:03 -06:00
AJ ONeal 64b2fd27d9 v0.15.2 2018-06-13 14:48:42 -06:00
AJ ONeal d9bca46510 add client-side otp 2018-06-13 14:48:29 -06:00
AJ ONeal bff71aa813 add client otp 2018-06-13 14:40:44 -06:00
AJ ONeal ee15f1be9c v0.15.1 2018-06-13 14:12:40 -06:00
AJ ONeal 94117c7edb add missing -servername to ssh over https docs 2018-06-13 14:12:23 -06:00
AJ ONeal 1b41498fb6 telebit ssh 22 2018-06-13 13:58:02 -06:00
AJ ONeal 71ffa3fb0e v0.15.0 2018-06-13 13:52:57 -06:00
AJ ONeal 1a7e519578 print config 2018-06-13 13:52:02 -06:00
AJ ONeal 7da7e4f07e use pathname not path, update token state 2018-06-13 13:38:59 -06:00
AJ ONeal aa35402c22 parse null/undefined correctly 2018-06-13 13:21:42 -06:00
AJ ONeal 252c2c80a4 parse true/false correctly 2018-06-13 13:20:08 -06:00
AJ ONeal 34ac5cf144 output and whitespace 2018-06-13 13:16:50 -06:00
AJ ONeal f17619efd2 output and whitespace 2018-06-13 13:12:38 -06:00
AJ ONeal 8a1d9feb26 typo bugfix 2018-06-13 13:04:11 -06:00
AJ ONeal 4bec2d1fa2 whitespace 2018-06-13 13:03:06 -06:00
AJ ONeal d0409417e2 whitespace 2018-06-13 13:01:43 -06:00
AJ ONeal 2b63d77dc9 whitespace 2018-06-13 12:59:44 -06:00
AJ ONeal ab405c586d stop tar file output 2018-06-13 12:57:35 -06:00
AJ ONeal d09118f8a3 workaround for https://github.com/nodejs/node/issues/21319 2018-06-13 12:54:33 -06:00
AJ ONeal c981544bad bugfix 2018-06-13 12:39:42 -06:00
AJ ONeal 27c1b0ceeb bugfix 2018-06-13 12:34:45 -06:00
AJ ONeal 70cb390579 bugfix 2018-06-13 12:16:09 -06:00
AJ ONeal 0572b0ebef bugfix 2018-06-13 12:07:28 -06:00
AJ ONeal 4b1ed54cdf bugfix 2018-06-13 11:50:46 -06:00
AJ ONeal 35bd82c04b remove output about symlink 2018-06-13 11:45:37 -06:00
AJ ONeal 788acbb7e9 bugfixes 2018-06-13 11:44:23 -06:00
AJ ONeal f70deb7f9d useTty when useTty is selected 2018-06-13 11:29:36 -06:00
AJ ONeal ad4ab2a06a useTty only when useTty is selected 2018-06-13 11:29:06 -06:00
AJ ONeal 1ff12d01f4 update question asking, fix some config stuff 2018-06-13 11:18:54 -06:00
AJ ONeal 753da7fa44 use /dev/tty for stdin when specified 2018-06-13 10:45:54 -06:00
AJ ONeal a98da102b9 move most user config into node 2018-06-12 04:36:37 -06:00
AJ ONeal e60f1fa14f fix bad if 2018-06-11 15:15:07 -06:00
AJ ONeal 23c40878e0 use correct config file 2018-06-11 15:09:20 -06:00
AJ ONeal d490ca7e4a also symlink telebitd 2018-06-11 15:04:29 -06:00
AJ ONeal f356cfac70 update instructional output 2018-06-11 15:00:14 -06:00
AJ ONeal cd37449e97 use TELEBIT_VERSION 2018-06-11 14:55:02 -06:00
AJ ONeal de7221ef37 separate remote from daemon 2018-06-11 14:52:01 -06:00
AJ ONeal 9cb6c61814 telebitd for daemon, cleanup, TELEBIT_VERSION 2018-06-11 13:14:36 -06:00
AJ ONeal a82f157b2e telebit remote, telebit daemon 2018-06-11 12:56:16 -06:00
AJ ONeal b03717c312 update windows instructions and error message 2018-06-09 15:52:15 -06:00
AJ ONeal 32e7f0af04 ssh and save now work 2018-06-09 14:00:47 -06:00
AJ ONeal 7266165fd6 launch after setting config 2018-06-09 13:12:30 -06:00
AJ ONeal a942587b80 bug in v10.3, use v10.2 2018-06-09 12:57:00 -06:00
AJ ONeal 34cee2dbac should restart on failure on mac 2018-06-09 11:26:30 -06:00
AJ ONeal 54e9977c47 rearrange inputs 2018-06-09 08:23:54 -06:00
AJ ONeal 87da6e0208 ask questions last 2018-06-09 08:19:57 -06:00
AJ ONeal 7530325d0a use node v10.3 (10.4 has a bug) 2018-06-09 08:08:11 -06:00
AJ ONeal 0afdb1526d macOS doesn't have /opt 2018-06-09 08:05:45 -06:00
AJ ONeal 4a089756f1 better output 2018-06-08 10:46:18 -06:00
AJ ONeal 1a77e6f740 v0.13.3 2018-06-08 10:33:06 -06:00
AJ ONeal 691cc3e56e write certs to correct path 2018-06-08 10:32:18 -06:00
AJ ONeal b6cb5837ed v0.13.2 2018-06-08 10:26:38 -06:00
AJ ONeal e61fde0831 restart on clean exit too 2018-06-08 10:25:48 -06:00
AJ ONeal 3cfe1cddc8 fix bash bin by passing all args 2018-06-08 10:11:26 -06:00
AJ ONeal 9d44093df5 how to activate an expired activation 2018-06-08 10:01:56 -06:00
AJ ONeal 5114d72151 clarify 2018-06-08 09:58:44 -06:00
AJ ONeal 06e3a84995 clarify some log output 2018-06-08 09:58:05 -06:00
AJ ONeal 637e772c83 v0.13.1 2018-06-08 03:02:20 -06:00
AJ ONeal 37fd4f2404 document commands 2018-06-08 03:01:59 -06:00
AJ ONeal 8567a1b6f9 lucky v0.13.0 2018-06-08 02:52:08 -06:00
AJ ONeal 5178da0330 new stuff appears to work 2018-06-08 02:50:00 -06:00
AJ ONeal b97239b252 more command and control 2018-06-08 00:46:07 -06:00
AJ ONeal ce7854b79d allow 'remote' restart 2018-06-07 16:09:30 -06:00
AJ ONeal e341cada1c Merge branch 'v1' 2018-06-07 10:50:42 -06:00
AJ ONeal 082d371f57 update whitespace 2018-06-07 10:50:35 -06:00
AJ ONeal ad2ac51b0a update installer 2018-06-07 10:43:05 -06:00
AJ ONeal 92969f5417 v0.12.0 2018-06-07 01:43:21 -06:00
AJ ONeal eef08eca54 notify user to check email 2018-06-07 01:42:10 -06:00
AJ ONeal 8282abc028 update access token 2018-06-07 01:27:56 -06:00
AJ ONeal 6e130ab612 show name of command which times out 2018-06-07 00:45:42 -06:00
AJ ONeal a5eb4e4a25 www.telebit.cloud => telebit.cloud 2018-06-07 00:11:06 -06:00
AJ ONeal 96983d53ca move intentional user messages and client data to CLI/UI 2018-06-07 00:08:26 -06:00
AJ ONeal d3a9d0422d add User Agent info 2018-06-06 03:24:14 -06:00
AJ ONeal 5725d9f1e1 old auth works again 2018-06-06 01:01:56 -06:00
AJ ONeal a94f3d26eb send auth info 2018-06-05 02:21:12 -06:00
AJ ONeal 092b7fe046 a little cleanup 2018-06-05 02:04:32 -06:00
AJ ONeal a59077e72d note log checking 2018-06-05 01:27:36 -06:00
AJ ONeal f442abae59 read relay prompt variable correctly 2018-06-05 01:22:10 -06:00
AJ ONeal df16948bc5 properly use mktemp 2018-06-05 01:13:38 -06:00
AJ ONeal a673e05087 update uninstaller 2018-06-05 01:07:32 -06:00
AJ ONeal 9a6c3d36f9 use nano as fallback editor 2018-06-05 01:02:43 -06:00
AJ ONeal cb6d7497f8 use only editor basename, not full path 2018-06-05 01:01:31 -06:00
AJ ONeal ae992c0bd1 fix bad comment 2018-06-05 00:55:59 -06:00
AJ ONeal fcbb21b223 add uninstall script 2018-06-05 00:53:20 -06:00
AJ ONeal 7c7dcc5703 fix sudo_cmd whitespace, editor detection, other things... 2018-06-05 00:46:06 -06:00
AJ ONeal 0ba0d7d087 fix conditional 2018-06-05 00:09:24 -06:00
AJ ONeal f19b162910 update uninstall instruction 2018-06-05 00:05:48 -06:00
AJ ONeal 74bc1e6a74 add uninstall instructions 2018-06-05 00:04:05 -06:00
AJ ONeal ab19162128 update docs 2018-06-04 23:50:02 -06:00
AJ ONeal d74529759c read prompt from /dev/tty 2018-06-04 23:23:59 -06:00
AJ ONeal 1d34541943 swap sudo with $sudo_cmd 2018-06-04 23:02:50 -06:00
AJ ONeal d6fcb467a9 used TELEBIT_PATH/usr/share, replace 'vim' with 'edit', change sudo_cmd 2018-06-04 23:00:28 -06:00
AJ ONeal ec3c0eb14a Update 'README.md' 2018-06-05 04:50:39 +00:00
AJ ONeal 3641eb3639 v0.11.0 2018-06-04 18:33:47 -06:00
AJ ONeal 3fe6880d34 update cli options 2018-06-04 18:33:24 -06:00
AJ ONeal 9f8ba5aa3c update docs a little bit 2018-06-04 18:32:18 -06:00
AJ ONeal fae1fe33b2 fix example config 2018-06-04 18:29:36 -06:00
AJ ONeal 462cfe41bb change default branch 2018-06-04 18:25:35 -06:00
AJ ONeal 3c7d89d3c4 update config examples 2018-06-04 18:23:33 -06:00
AJ ONeal ca6feb8193 show /opt, not /etc 2018-06-05 00:16:15 +00:00
AJ ONeal ed1daccbf9 whitespace fix 2018-06-05 00:14:49 +00:00
AJ ONeal deb9873333 update symlink to system config 2018-06-05 00:13:03 +00:00
AJ ONeal 50a8536bff fix agreeTos config typo 2018-06-04 23:53:26 +00:00
AJ ONeal dada009a62 agree_tos is dependent on email 2018-06-04 23:52:22 +00:00
AJ ONeal 01e77f4959 add agree_tos 2018-06-04 23:50:34 +00:00
AJ ONeal 8a41545d17 fix echo newline expansion 2018-06-04 23:48:17 +00:00
AJ ONeal 9ae6143857 add missing 'relay' config 2018-06-04 23:43:11 +00:00
AJ ONeal f8e2546282 use default relay 2018-06-02 04:05:12 -06:00
AJ ONeal 1cd02940d6 fix config bug 2018-06-02 04:00:08 -06:00
AJ ONeal b070024177 seems to work with launchd 2018-06-02 03:57:38 -06:00
AJ ONeal fda2d05e81 got further 2018-06-02 03:31:31 -06:00
AJ ONeal f251ed226f fixes 2018-06-02 03:30:13 -06:00
AJ ONeal 6e8f4c8d98 rename installer 2018-06-02 03:05:17 -06:00
AJ ONeal 0b4f2c67f1 fix remote require path 2018-06-02 02:59:53 -06:00
AJ ONeal 184d4a0ed1 lots up cleanup 2018-06-02 02:58:42 -06:00
AJ ONeal 2b9d70a31e a place for apps 2018-06-02 02:28:58 -06:00
AJ ONeal 4bfd3afba8 explain installer, update system services 2018-06-02 02:25:41 -06:00
AJ ONeal 720fc71bea update installer 2018-06-01 13:14:59 -06:00
AJ ONeal 4658fa88c6 echo tcp 2018-06-01 03:50:27 -06:00
AJ ONeal 588373e044 update grant message 2018-06-01 03:34:55 -06:00
AJ ONeal 0aab6c8739 update grant message 2018-06-01 03:13:04 -06:00
AJ ONeal af9a7c5812 put ssh detection on full auto 2018-06-01 02:45:38 -06:00
AJ ONeal 4368569b25 ssh detection works 2018-06-01 02:10:27 -06:00
AJ ONeal 5abff544e9 ws 2018-06-01 01:36:49 -06:00
AJ ONeal c54f121a85 small refactor 2018-06-01 01:36:29 -06:00
AJ ONeal 8c1c8d8006 cleanup 2018-05-31 14:48:12 -06:00
AJ ONeal 5207012830 keep address, localPort (service), and encrypted 2018-05-31 05:24:58 -06:00
AJ ONeal acaf22c200 working cleanup 2018-05-31 04:10:47 -06:00
AJ ONeal e848e1466b update systemd configs 2018-05-31 02:38:01 -06:00
AJ ONeal eada75a83e refactor and upgrade to proxy-packer 2018-05-31 00:19:53 -06:00
AJ ONeal 0c3f78147e handle connection within telebit 2018-05-29 03:00:25 -06:00
AJ ONeal 4d8e5d434b accept given token or secret-generated token 2018-05-29 01:45:47 -06:00
AJ ONeal 3cf8c238d0 warn when no tokens given 2018-05-29 01:44:50 -06:00
AJ ONeal d4c52787b9 getting closer... 2018-05-27 05:02:19 -06:00
AJ ONeal 9d6384addc fix whitespace 2018-05-27 04:38:29 -06:00
AJ ONeal cd07820cd9 rename lib file 2018-05-27 04:32:00 -06:00
AJ ONeal b814beb6bd progress towards telebit 2018-05-27 04:26:34 -06:00
AJ ONeal 61947a492d merge 2018-05-27 01:59:43 -06:00
AJ ONeal 28955f8e85 moving to telebit 2018-05-27 01:58:37 -06:00
41 changed files with 4974 additions and 664 deletions

15
.gitignore vendored
View File

@ -1,4 +1,19 @@
node_modules.* 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
logs logs

206
LICENSE
View File

@ -1,198 +1,38 @@
Apache License Copyright 2016 AJ ONeal
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION This is open source software; you can redistribute it and/or modify it under the
terms of either:
1. Definitions. a) the "MIT License"
b) the "Apache-2.0 License"
"License" shall mean the terms and conditions for use, reproduction, MIT License
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by Permission is hereby granted, free of charge, to any person obtaining a copy
the copyright owner that is granting the License. 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:
"Legal Entity" shall mean the union of the acting entity and all The above copyright notice and this permission notice shall be included in all
other entities that control, are controlled by, or are under common copies or substantial portions of the Software.
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.
"You" (or "Your") shall mean an individual or Legal Entity THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
exercising permissions granted by this License. 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.
"Source" form shall mean the preferred form for making modifications, Apache-2.0 License Summary
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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at 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 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,

465
README.md
View File

@ -1,98 +1,354 @@
# Telebit™ # Telebit™ Remote
A client that works in combination with [stunneld.js](https://git.coolaj86.com/coolaj86/tunnel-server.js) Because friends don't let friends localhost™
to allow you to serve http and https from any computer, anywhere through a secure tunnel.
| Sponsored by [ppl](https://ppl.family) | [Telebit Relay](https://git.coolaj86.com/coolaj86/telebitd.js) | **Telebit** | | 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**.
* CLI * CLI
* Library * Node.js Library
* Browser Library
CLI Telebit CLI
=== -----------
Installs as `stunnel.js` with the alias `jstunnel` Installs Telebit Remote as `telebit`
(for those that regularly use `stunnel` but still like commandline completion). (for those that regularly use `telebit` but still like commandline completion).
### Install ### Install
```bash ```bash
npm install -g 'git+https://git@git.coolaj86.com/coolaj86/tunnel-client.js.git#v1' npm install -g telebit
```
```bash
npm install -g 'https://git.coolaj86.com/coolaj86/telebit.js.git#v1'
``` ```
Or if you want to bow down to the kings of the centralized dictator-net: Or if you want to bow down to the kings of the centralized dictator-net:
```bash How to use Telebit Remote with your own instance of Telebit Relay:
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 ```bash
stunnel.js --agree-tos --email john@example.com --locals http:*:4080,https:*:8443 --device telebitd \
```
```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>> \ --locals <<external domain name>> \
--stunneld wss://<<tunnel domain>>:<<tunnel port>> \ --relay wss://<<tunnel domain>>:<<tunnel port>> \
--secret <<128-bit hex key>> --secret <<128-bit hex key>>
``` ```
```bash ```bash
stunnel.js --locals john.example.com --stunneld wss://tunnel.example.com:443 --secret abc123 telebitd --locals john.example.com --relay wss://tunnel.example.com:443 --secret abc123
``` ```
```bash ```bash
stunnel.js \ telebitd \
--locals <<protocol>>:<<external domain name>>:<<local port>> \ --locals <<protocol>>:<<external domain name>>:<<local port>> \
--stunneld wss://<<tunnel domain>>:<<tunnel port>> \ --relay wss://<<tunnel domain>>:<<tunnel port>> \
--secret <<128-bit hex key>> --secret <<128-bit hex key>>
``` ```
```bash ```bash
stunnel.js \ telebitd \
--locals http:john.example.com:3000,https:john.example.com \ --locals http:john.example.com:3000,https:john.example.com \
--stunneld wss://tunnel.example.com:443 \ --relay wss://tunnel.example.com:443 \
--secret abc123 --secret abc123
``` ```
``` ```
--secret the same secret used by stunneld (used for authentication) --secret the same secret used by the Telebit Relay (for authentication)
--locals comma separated list of <proto>:<servername>:<port> to which --locals comma separated list of <proto>:<servername>:<port> to which
incoming http and https should be forwarded incoming http and https should be forwarded
--stunneld the domain or ip address at which you are running stunneld.js --relay the domain or ip address at which you are running Telebit Relay
-k, --insecure ignore invalid ssl certificates from stunneld -k, --insecure ignore invalid ssl certificates from relay
``` ```
Library Node.js Library
======= =======
### Example ### Example
```javascript ```javascript
var stunnel = require('stunnel'); var Telebit = require('telebit');
stunnel.connect({ Telebit.connect({
stunneld: 'wss://tunnel.example.com' relay: 'wss://tunnel.example.com'
, token: '...' , token: '...'
, locals: [ , locals: [
// defaults to sending http to local port 80 and https to local port 443 // defaults to sending http to local port 80 and https to local port 443
@ -132,7 +388,7 @@ local handler and the tunnel handler.
You could do a little magic like this: You could do a little magic like this:
```js ```js
stunnel.connect({ Telebit.connect({
// ... // ...
, net: { , net: {
createConnection: function (info, cb) { createConnection: function (info, cb) {
@ -166,3 +422,108 @@ stunnel.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

View File

@ -1,6 +0,0 @@
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)

View File

@ -1,292 +0,0 @@
#!/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 Executable file
View File

@ -0,0 +1,832 @@
#!/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 Executable file
View File

@ -0,0 +1,866 @@
#!/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);
}());

0
etc/.gitkeep Normal file
View File

20
examples/telebit.full.yml Normal file
View File

@ -0,0 +1,20 @@
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/'

View File

@ -0,0 +1,7 @@
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

310
lib/cli-common.js Normal file
View File

@ -0,0 +1,310 @@
'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);
}
};

View File

@ -0,0 +1,19 @@
'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.

After

Width:  |  Height:  |  Size: 43 B

42
lib/html/index.html Normal file
View File

@ -0,0 +1,42 @@
<!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>

49
lib/html/js/app.js Normal file
View File

@ -0,0 +1,49 @@
(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;
});
}
}());

View File

@ -1,10 +1,16 @@
(function () { (function () {
'use strict'; 'use strict';
var PromiseA;
try {
PromiseA = require('bluebird');
} catch(e) {
PromiseA = global.Promise;
}
var WebSocket = require('ws'); var WebSocket = require('ws');
var PromiseA = require('bluebird');
var sni = require('sni'); var sni = require('sni');
var Packer = require('tunnel-packer'); var Packer = require('proxy-packer');
var os = require('os');
function timeoutPromise(duration) { function timeoutPromise(duration) {
return new PromiseA(function (resolve) { return new PromiseA(function (resolve) {
@ -12,30 +18,41 @@ function timeoutPromise(duration) {
}); });
} }
function run(copts) { function _connect(state) {
// jshint latedef:false // jshint latedef:false
var activityTimeout = copts.activityTimeout || (2*60 - 5)*1000; var defaultHttpTimeout = (2 * 60);
var pongTimeout = copts.pongTimeout || 10*1000; var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
var pongTimeout = state.pongTimeout || 10*1000;
// Allow the tunnel client to be created with no token. This will prevent the connection from // 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 // 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. // they can get a promise that will provide feedback about invalid tokens.
var tokens = []; var tokens = [];
if (copts.token) { var auth;
tokens.push(copts.token); 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);
} }
var wstunneler; var wstunneler;
var authenticated = false; var authenticated = false;
var authsent = false;
var initialConnect = true;
var localclients = {}; var localclients = {};
var pausedClients = []; var pausedClients = [];
var clientHandlers = { var clientHandlers = {
add: function (conn, cid, opts, servername) { add: function (conn, cid, tun) {
localclients[cid] = conn; localclients[cid] = conn;
console.info("[connect] new client '" + cid + "' for '" + servername + "' (" + clientHandlers.count() + " clients)"); console.info("[connect] new client '" + cid + "' for '" + tun.name + ":" + tun.serviceport + "' "
+ "(" + clientHandlers.count() + " clients)");
conn.tunnelCid = cid; conn.tunnelCid = cid;
conn.tunnelRead = opts.data.byteLength; conn.tunnelRead = tun.data.byteLength;
conn.tunnelWritten = 0; conn.tunnelWritten = 0;
conn.on('data', function onLocalData(chunk) { conn.on('data', function onLocalData(chunk) {
@ -50,7 +67,7 @@ function run(copts) {
// down the data we are getting to send over. We also want to pause all active connections // 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 // 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. // stuff waiting for all other connections to finish because it tried writing near the border.
var bufSize = wsHandlers.sendMessage(Packer.pack(opts, chunk)); var bufSize = wsHandlers.sendMessage(Packer.pack(tun, chunk));
if (pausedClients.length || bufSize > 1024*1024) { if (pausedClients.length || bufSize > 1024*1024) {
// console.log('[onLocalData] paused connection', cid, 'to allow websocket to catch up'); // console.log('[onLocalData] paused connection', cid, 'to allow websocket to catch up');
conn.pause(); conn.pause();
@ -63,14 +80,14 @@ function run(copts) {
console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon"); console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon");
conn.tunnelClosing = true; conn.tunnelClosing = true;
if (!sentEnd) { if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(opts, null, 'end')); wsHandlers.sendMessage(Packer.pack(tun, null, 'end'));
sentEnd = true; sentEnd = true;
} }
}); });
conn.on('error', function onLocalError(err) { conn.on('error', function onLocalError(err) {
console.info("[onLocalError] connection '" + cid + "' errored:", err); console.info("[onLocalError] connection '" + cid + "' errored:", err);
if (!sentEnd) { if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(opts, {message: err.message, code: err.code}, 'error')); wsHandlers.sendMessage(Packer.pack(tun, {message: err.message, code: err.code}, 'error'));
sentEnd = true; sentEnd = true;
} }
}); });
@ -78,7 +95,7 @@ function run(copts) {
delete localclients[cid]; delete localclients[cid];
console.log('[onLocalClose] closed "' + cid + '" read:'+conn.tunnelRead+', wrote:'+conn.tunnelWritten+' (' + clientHandlers.count() + ' clients)'); console.log('[onLocalClose] closed "' + cid + '" read:'+conn.tunnelRead+', wrote:'+conn.tunnelWritten+' (' + clientHandlers.count() + ' clients)');
if (!sentEnd) { if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(opts, null, hadErr && 'error' || 'end')); wsHandlers.sendMessage(Packer.pack(tun, null, hadErr && 'error' || 'end'));
sentEnd = true; sentEnd = true;
} }
}); });
@ -169,11 +186,12 @@ function run(copts) {
function sendCommand(name) { function sendCommand(name) {
var id = Math.ceil(1e9 * Math.random()); var id = Math.ceil(1e9 * Math.random());
var cmd = [id, name].concat(Array.prototype.slice.call(arguments, 1)); 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')); wsHandlers.sendMessage(Packer.pack(null, cmd, 'control'));
setTimeout(function () { setTimeout(function () {
if (pendingCommands[id]) { if (pendingCommands[id]) {
console.warn('command', id, 'timed out'); console.warn('command', name, id, 'timed out');
pendingCommands[id]({ pendingCommands[id]({
message: 'response not received in time' message: 'response not received in time'
, code: 'E_TIMEOUT' , code: 'E_TIMEOUT'
@ -194,7 +212,13 @@ function run(copts) {
} }
function sendAllTokens() { function sendAllTokens() {
if (auth) {
authsent = true;
sendCommand('auth', auth).catch(function (err) { console.error('1', err); });
}
tokens.forEach(function (jwtoken) { tokens.forEach(function (jwtoken) {
if (state.debug) { console.log('[DEBUG] send token'); }
authsent = true;
sendCommand('add_token', jwtoken) sendCommand('add_token', jwtoken)
.catch(function (err) { .catch(function (err) {
console.error('failed re-adding token', jwtoken, 'after reconnect', err); console.error('failed re-adding token', jwtoken, 'after reconnect', err);
@ -205,6 +229,11 @@ function run(copts) {
}); });
} }
function noHandler(cmd) {
console.warn("[telebit] state.handlers['" + cmd[1] + "'] not set");
console.warn(cmd[2]);
}
var connCallback; var connCallback;
var packerHandlers = { var packerHandlers = {
@ -237,8 +266,7 @@ function run(copts) {
} }
if (cmd[1] === 'hello') { if (cmd[1] === 'hello') {
// We only get the 'hello' event after the token has been validated if (state.debug) { console.log('[DEBUG] hello received'); }
authenticated = true;
sendAllTokens(); sendAllTokens();
if (connCallback) { if (connCallback) {
connCallback(); connCallback();
@ -246,90 +274,58 @@ function run(copts) {
// TODO: handle the versions and commands provided by 'hello' - isn't super important // TODO: handle the versions and commands provided by 'hello' - isn't super important
// yet since there is only one version and set of commands. // yet since there is only one version and set of commands.
err = null; err = null;
} } else if (cmd[1] === 'grant') {
else { 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 {
err = { message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' }; err = { message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' };
} }
wsHandlers.sendMessage(Packer.pack(null, [-cmd[0], err], 'control')); wsHandlers.sendMessage(Packer.pack(null, [-cmd[0], err], 'control'));
} }
, onmessage: function (opts) { , onmessage: function (tun) {
var net = copts.net || require('net'); var cid = tun._id = Packer.addrToId(tun);
var cid = Packer.addrToId(opts);
var service = opts.service.toLowerCase();
var portList = copts.services[service];
var servername;
var port;
var str; var str;
var m; var m;
if (clientHandlers.write(cid, opts)) { if ('http' === tun.service) {
return; str = tun.data.toString();
}
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); m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
servername = (m && m[1].toLowerCase() || '').split(':')[0]; tun._name = tun._hostname = (m && m[1].toLowerCase() || '').split(':')[0];
} }
else if ('https' === service) { else if ('https' === tun.service || 'tls' === tun.service) {
servername = sni(opts.data); tun._name = tun._servername = sni(tun.data);
} } else {
else { tun._name = '';
servername = '*';
} }
if (!servername) { if (clientHandlers.write(cid, tun)) { return; }
//console.warn(opts.data.toString());
packerHandlers._onConnectError(cid, opts, new Error("missing servername for '" + cid + "' " + opts.data.byteLength));
return;
}
port = portList[servername]; wstunneler.pause();
if (!port) { require(state.sortingHat).assign(state, tun, function (err, conn) {
// Check for any wildcard domains, sorted longest to shortest so the one with the if (err) {
// biggest natural match will be found first. err.message = err.message.replace(/:tun_id/, tun._id);
Object.keys(portList).filter(function (pattern) { packerHandlers._onConnectError(cid, tun, err);
return pattern[0] === '*' && pattern.length > 1; return;
}).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) { , onpause: function (opts) {
@ -415,8 +411,9 @@ function run(copts) {
} }
, onOpen: function () { , onOpen: function () {
console.info("[open] connected to '" + copts.stunneld + "'"); console.info("[open] connected to '" + (state.wss || state.relay) + "'");
wsHandlers.refreshTimeout(); wsHandlers.refreshTimeout();
timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout); timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout);
wstunneler._socket.on('drain', function () { wstunneler._socket.on('drain', function () {
@ -438,10 +435,17 @@ function run(copts) {
pausedClients.length = 0; 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 () { , onClose: function () {
console.log('ON CLOSE');
clearTimeout(timeoutId); clearTimeout(timeoutId);
wstunneler = null; wstunneler = null;
clientHandlers.closeAll(); clientHandlers.closeAll();
@ -456,16 +460,35 @@ function run(copts) {
} }
if (!authenticated) { 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.'); console.info('[close] failed on first attempt... check authentication.');
timeoutId = null; timeoutId = null;
} }
else if (tokens.length) { else if (tokens.length) {
if(state.handlers.onDisconnect) {
state.handlers.onDisconnect();
}
console.info('[retry] disconnected and waiting...'); console.info('[retry] disconnected and waiting...');
timeoutId = setTimeout(connect, 5000); timeoutId = setTimeout(connect, 5000);
} else {
if(state.handlers.onClose) {
state.handlers.onClose();
}
} }
} }
, onError: function (err) { , 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("[tunnel error] " + err.message);
console.error(err); console.error(err);
if (connCallback) { if (connCallback) {
@ -490,19 +513,33 @@ function run(copts) {
}; };
function connect() { function connect() {
if (!tokens.length) {
return;
}
if (wstunneler) { if (wstunneler) {
console.warn('attempted to connect with connection already active'); console.warn('attempted to connect with connection already active');
return; 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; timeoutId = null;
var machine = require('tunnel-packer').create(packerHandlers); var machine = Packer.create(packerHandlers);
console.info("[connect] '" + copts.stunneld + "'"); console.info("[connect] '" + (state.wss || state.relay) + "'");
var tunnelUrl = copts.stunneld.replace(/\/$/, '') + '/?access_token=' + tokens[0]; var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !copts.insecure }); wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
wstunneler.on('open', wsHandlers.onOpen); wstunneler.on('open', wsHandlers.onOpen);
wstunneler.on('close', wsHandlers.onClose); wstunneler.on('close', wsHandlers.onClose);
wstunneler.on('error', wsHandlers.onError); wstunneler.on('error', wsHandlers.onError);
@ -523,7 +560,7 @@ function run(copts) {
var connPromise; var connPromise;
return { return {
end: function() { end: function(cb) {
tokens.length = 0; tokens.length = 0;
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@ -532,7 +569,7 @@ function run(copts) {
if (wstunneler) { if (wstunneler) {
try { try {
wstunneler.close(); wstunneler.close(cb);
} catch(e) { } catch(e) {
console.error("[error] wstunneler.close()"); console.error("[error] wstunneler.close()");
console.error(e); console.error(e);
@ -540,6 +577,12 @@ function run(copts) {
} }
} }
, append: function (token) { , 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) { if (tokens.indexOf(token) >= 0) {
return PromiseA.resolve(); return PromiseA.resolve();
} }
@ -613,7 +656,7 @@ function run(copts) {
}; };
} }
module.exports.connect = run; module.exports.connect = _connect;
module.exports.createConnection = run; module.exports.createConnection = _connect;
}()); }());

551
lib/sorting-hat.js Normal file
View File

@ -0,0 +1,551 @@
'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 Normal file
View File

@ -0,0 +1,140 @@
'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);
};
};

View File

@ -1,19 +1,23 @@
{ {
"name": "stunnel", "name": "telebit",
"version": "0.10.4", "version": "0.19.26",
"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.", "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": "wsclient.js", "main": "lib/remote.js",
"files": [
"bin",
"lib",
"usr"
],
"bin": { "bin": {
"jstunnel": "bin/stunnel.js", "telebit": "bin/telebit.js",
"stunnel.js": "bin/stunnel.js", "telebitd": "bin/telebitd.js"
"stunnel-js": "bin/stunnel.js"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+ssh://git@git.coolaj86.com/coolaj86/tunnel-client.js.git" "url": "https://git.coolaj86.com/coolaj86/telebit.js.git"
}, },
"keywords": [ "keywords": [
"cli", "cli",
@ -32,6 +36,9 @@
"tunnel", "tunnel",
"localtunnel", "localtunnel",
"localtunnel.me", "localtunnel.me",
"underpass",
"ngrok",
"ngrok.io",
"proxy", "proxy",
"reverse", "reverse",
"reverse-proxy", "reverse-proxy",
@ -42,16 +49,30 @@
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)", "license": "(MIT OR Apache-2.0)",
"bugs": { "bugs": {
"url": "https://git.coolaj86.com/coolaj86/tunnel-client.js/issues" "url": "https://git.coolaj86.com/coolaj86/telebit.js/issues"
}, },
"homepage": "https://git.coolaj86.com/coolaj86/tunnel-client.js#readme", "homepage": "https://git.coolaj86.com/coolaj86/telebit.js#readme",
"dependencies": { "dependencies": {
"bluebird": "^3.5.0", "@coolaj86/urequest": "^1.3.5",
"commander": "^2.9.0", "finalhandler": "^1.1.1",
"greenlock": "^2.3.1",
"js-yaml": "^3.11.0",
"jsonwebtoken": "^7.1.9", "jsonwebtoken": "^7.1.9",
"oauth3.js": "git+https://git.oauth3.org/OAuth3/oauth3.js.git#v1", "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",
"sni": "^1.0.0", "sni": "^1.0.0",
"tunnel-packer": "^1.2.0", "socket-pair": "^1.0.3",
"ws": "^2.2.3" "ws": "^2.2.3"
},
"trulyOptionalDependencies": {
"bluebird": "^3.5.1"
},
"enginesStrict": true,
"engines": {
"node": "10.2.1 10.4 10.6"
} }
} }

View File

@ -1,19 +0,0 @@
(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());
});
}());

21
tests/README.md Normal file
View File

@ -0,0 +1,21 @@
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

27
tests/echo.js Normal file
View File

@ -0,0 +1,27 @@
'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');
});

37
tests/pair-request.js Normal file
View File

@ -0,0 +1,37 @@
'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: '...'
}
*/
});

22
tests/pair-state.js Normal file
View File

@ -0,0 +1,22 @@
'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
*/
});

20
tests/windows-pipe.js Normal file
View File

@ -0,0 +1,20 @@
'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());
});

View File

@ -0,0 +1,22 @@
'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');
});

0
usr/share/.gitkeep Normal file
View File

View File

@ -0,0 +1,61 @@
<?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>

2
usr/share/dist/bin/telebit.tpl vendored Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
{TELEBIT_NODE} {TELEBIT_JS} "$@"

2
usr/share/dist/bin/telebitd.tpl vendored Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
{TELEBIT_NODE} {TELEBITD_JS} daemon "$@"

View File

@ -0,0 +1,64 @@
# 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

View File

@ -0,0 +1,70 @@
<?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>

View File

@ -0,0 +1,64 @@
# 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

View File

@ -0,0 +1,306 @@
'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.");
});
}

85
usr/share/install.sh Normal file
View File

@ -0,0 +1,85 @@
#!/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

560
usr/share/install_helper.sh Normal file
View File

@ -0,0 +1,560 @@
#!/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

View File

@ -0,0 +1,46 @@
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

View File

@ -0,0 +1,5 @@
#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

View File

@ -0,0 +1,97 @@
'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();
}

0
var/log/.gitkeep Normal file
View File

0
var/run/.gitkeep Normal file
View File