2016-09-30 17:47:43 +00:00
#!/usr/bin/env node
2016-09-30 16:33:38 +00:00
( function ( ) {
'use strict' ;
var pkg = require ( '../package.json' ) ;
2018-05-31 10:10:47 +00:00
console . log ( pkg . name , pkg . version ) ;
2016-09-30 16:33:38 +00:00
2016-09-30 19:04:27 +00:00
var url = require ( 'url' ) ;
2018-06-02 08:59:53 +00:00
var remote = require ( '../' ) ;
2018-05-27 11:02:19 +00:00
var state = { } ;
2018-05-27 10:26:34 +00:00
var argv = process . argv . slice ( 2 ) ;
var confIndex = argv . indexOf ( '--config' ) ;
var confpath ;
if ( - 1 === confIndex ) {
confIndex = argv . indexOf ( '-c' ) ;
}
confpath = argv [ confIndex + 1 ] ;
function help ( ) {
2018-05-27 11:02:19 +00:00
console . info ( '' ) ;
console . info ( 'Telebit Remote v' + pkg . version ) ;
2018-05-27 10:26:34 +00:00
console . info ( '' ) ;
console . info ( 'Usage:' ) ;
console . info ( '' ) ;
console . info ( '\ttelebit --config <path>' ) ;
console . info ( '' ) ;
console . info ( 'Example:' ) ;
console . info ( '' ) ;
console . info ( '\ttelebit --config /etc/telebit/telebit.yml' ) ;
console . info ( '' ) ;
console . info ( 'Config:' ) ;
console . info ( '' ) ;
console . info ( '\tSee https://git.coolaj86.com/coolaj86/telebit.js' ) ;
console . info ( '' ) ;
console . info ( '' ) ;
process . exit ( 0 ) ;
}
2018-05-31 10:10:47 +00:00
if ( - 1 === confIndex ) {
confpath = require ( 'path' ) . join ( require ( 'os' ) . homedir ( ) , '.config/telebit/telebit.yml' ) ;
console . info ( 'Using default --config "' + confpath + '"' ) ;
}
if ( - 1 !== argv . indexOf ( '-h' ) || - 1 !== argv . indexOf ( '--help' ) ) {
2018-05-27 10:26:34 +00:00
help ( ) ;
}
if ( ! confpath || /^--/ . test ( confpath ) ) {
help ( ) ;
}
2016-09-30 16:33:38 +00:00
2018-05-27 11:02:19 +00:00
require ( 'fs' ) . readFile ( confpath , 'utf8' , function ( err , text ) {
var config ;
var recase = require ( 'recase' ) . create ( { } ) ;
var camelCopy = recase . camelCopy . bind ( recase ) ;
if ( err ) {
console . error ( "\nCouldn't load config:\n\n\t" + err . message + "\n" ) ;
process . exit ( 1 ) ;
return ;
}
try {
config = JSON . parse ( text ) ;
} catch ( e1 ) {
try {
config = require ( 'js-yaml' ) . safeLoad ( text ) ;
} catch ( e2 ) {
console . error ( e1 . message ) ;
console . error ( e2 . message ) ;
process . exit ( 1 ) ;
return ;
}
}
state . config = camelCopy ( config ) ;
rawTunnel ( ) ;
} ) ;
function connectTunnel ( ) {
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 ;
}
} ;
2018-05-31 10:10:47 +00:00
state . greenlock = state . config . greenlock || { } ;
if ( ! state . config . sortingHat ) {
state . config . sortingHat = './lib/sorting-hat.js' ;
2018-05-31 06:19:53 +00:00
}
2018-05-31 10:10:47 +00:00
state . config . sortingHat = require ( 'path' ) . resolve ( _ _dirname , '..' , state . config . sortingHat ) ;
2018-05-27 11:02:19 +00:00
2018-05-29 09:00:25 +00:00
// TODO Check undefined vs false for greenlock config
2018-05-27 11:02:19 +00:00
var tun = remote . connect ( {
relay : state . config . relay
2018-05-31 06:19:53 +00:00
, config : state . config
2018-05-31 10:10:47 +00:00
, sortingHat : state . config . sortingHat
2018-05-27 11:02:19 +00:00
, net : state . net
, insecure : state . config . relay _ignore _invalid _certificates
2018-05-29 07:45:47 +00:00
, token : state . token
2018-05-29 09:00:25 +00:00
, greenlockConfig : {
version : state . greenlock . version || 'draft-11'
, server : state . greenlock . server || 'https://acme-v02.api.letsencrypt.org/directory'
, communityMember : state . greenlock . communityMember || state . config . communityMember
, telemetry : state . greenlock . telemetry || state . config . telemetry
, configDir : state . greenlock . configDir || '~/acme/etc/'
// TODO, store: require(state.greenlock.store.name || 'le-store-certbot').create(state.greenlock.store.options || {})
, approveDomains : function ( opts , certs , cb ) {
2018-05-31 10:10:47 +00:00
console . log ( "trying approve domains" ) ;
2018-05-29 09:00:25 +00:00
// Certs being renewed are listed in certs.altnames
if ( certs ) {
opts . domains = certs . altnames ;
cb ( null , { options : opts , certs : certs } ) ;
return ;
}
2018-05-31 10:10:47 +00:00
// by virtue of the fact that it's being tunneled through a
// trusted source that is already checking, we're good
//if (-1 !== state.config.servernames.indexOf(opts.domains[0])) {
2018-05-29 09:00:25 +00:00
opts . email = state . greenlock . email || state . config . email ;
2018-06-04 23:53:26 +00:00
opts . agreeTos = state . greenlock . agree || state . config . agreeTos ;
2018-05-29 09:00:25 +00:00
cb ( null , { options : opts , certs : certs } ) ;
return ;
2018-05-31 10:10:47 +00:00
//}
//cb(new Error("servername not found in allowed list"));
2018-05-29 09:00:25 +00:00
}
}
2018-05-27 11:02:19 +00:00
} ) ;
2018-05-31 10:10:47 +00:00
require ( state . config . sortingHat ) . print ( state . config ) ;
2018-05-27 11:02:19 +00:00
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 ( ) {
if ( ! state . config . relay ) {
throw new Error ( "config is missing 'relay'" ) ;
}
if ( ! ( state . config . secret || state . config . token ) ) {
console . error ( "You must use --secret or --token with --relay" ) ;
process . exit ( 1 ) ;
return ;
}
var location = url . parse ( state . config . relay ) ;
if ( ! location . protocol || /\./ . test ( location . protocol ) ) {
state . config . relay = 'wss://' + state . config . relay ;
location = url . parse ( state . config . relay ) ;
}
var aud = location . hostname + ( location . port ? ':' + location . port : '' ) ;
state . config . relay = location . protocol + '//' + aud ;
if ( ! state . config . token ) {
var jwt = require ( 'jsonwebtoken' ) ;
var tokenData = {
2018-05-31 10:10:47 +00:00
domains : Object . keys ( state . config . servernames ) . filter ( function ( name ) { return /\./ . test ( name ) ; } )
2018-05-27 11:02:19 +00:00
, aud : aud
, iss : Math . round ( Date . now ( ) / 1000 )
} ;
state . token = jwt . sign ( tokenData , state . config . secret ) ;
}
state . token = state . token || state . config . token ;
connectTunnel ( ) ;
}
/ *
2017-09-08 16:54:47 +00:00
var domainsMap = { } ;
var services = { } ;
2017-03-30 01:29:37 +00:00
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 ;
}
2016-10-11 23:43:24 +00:00
function collectProxies ( val , memo ) {
var vals = val . split ( /,/g ) ;
function parseProxy ( location ) {
2017-03-28 21:31:45 +00:00
// john.example.com
// https:3443
2016-10-11 23:43:24 +00:00
// http:john.example.com:3000
// http://john.example.com:3000
var parts = location . split ( ':' ) ;
var dual = false ;
2017-03-28 21:31:45 +00:00
if ( 1 === parts . length ) {
// john.example.com -> :john.example.com:0
2016-10-11 23:43:24 +00:00
parts [ 1 ] = parts [ 0 ] ;
2017-03-28 21:31:45 +00:00
parts [ 0 ] = '' ;
parts [ 2 ] = 0 ;
2016-10-11 23:43:24 +00:00
dual = true ;
2016-10-06 05:27:57 +00:00
}
2017-03-28 21:31:45 +00:00
else if ( 2 === parts . length ) {
// https:3443 -> https:*:3443
parts [ 2 ] = parts [ 1 ] ;
parts [ 1 ] = '*' ;
}
2016-10-11 23:43:24 +00:00
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?
2017-03-28 21:31:45 +00:00
if ( ! parts [ 0 ] || 'http' === parts [ 0 ] ) {
2016-10-11 23:43:24 +00:00
parts [ 2 ] = 80 ;
}
else if ( 'https' === parts [ 0 ] ) {
parts [ 2 ] = 443 ;
}
else {
throw new Error ( "port must be specified - ex: tls:*:1337" ) ;
}
2016-09-30 16:33:38 +00:00
}
2016-10-11 23:43:24 +00:00
memo . push ( {
2017-03-28 21:31:45 +00:00
protocol : parts [ 0 ] || 'https'
2016-10-11 23:43:24 +00:00
, hostname : parts [ 1 ]
2017-03-28 21:31:45 +00:00
, port : parts [ 2 ] || 443
2016-10-11 23:43:24 +00:00
} ) ;
if ( dual ) {
memo . push ( {
protocol : 'http'
, hostname : parts [ 1 ]
2017-03-28 21:31:45 +00:00
, port : 80
2016-10-11 23:43:24 +00:00
} ) ;
2016-10-06 21:01:58 +00:00
}
}
2016-10-11 23:43:24 +00:00
vals . map ( function ( val ) {
return parseProxy ( val ) ;
2016-09-30 19:04:27 +00:00
} ) ;
2016-09-30 16:33:38 +00:00
return memo ;
}
2018-05-27 10:26:34 +00:00
var program = require ( 'commander' ) ;
program
. version ( pkg . version )
//.command('jsurl <url>')
. arguments ( '<url>' )
. action ( function ( url ) {
program . url = url ;
} )
. option ( '-k --insecure' , 'Allow TLS connections to a Telebit Relay 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 ( '--relay <URL>' , 'the domain (or ip address) at which you are running Telebit Relay (the proxy)' ) // --proxy
. option ( '--secret <STRING>' , 'the same secret used by the Telebit Relay (used for JWT authentication)' )
. option ( '--token <STRING>' , 'a pre-generated token for use with the Telebit Relay (instead of generating one with --secret)' )
. option ( '--agree-tos' , 'agree to the Telebit 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 )
;
2017-03-26 07:37:26 +00:00
2017-04-03 21:11:59 +00:00
program . locals = ( program . locals || [ ] ) . concat ( program . domains || [ ] ) ;
2017-03-30 01:29:37 +00:00
program . locals . forEach ( function ( proxy ) {
2017-04-03 21:11:59 +00:00
// 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 ;
2017-03-30 01:29:37 +00:00
}
2017-04-03 21:11:59 +00:00
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 ;
}
2017-03-30 01:29:37 +00:00
}
}
} ) ;
2017-04-03 21:11:59 +00:00
if ( Object . keys ( domainsMap ) . length === 0 ) {
console . error ( 'no domains specified' ) ;
process . exit ( 1 ) ;
return ;
2017-03-28 21:31:45 +00:00
}
2016-10-06 05:27:57 +00:00
2017-04-03 21:11:59 +00:00
// 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 ;
2018-05-27 11:02:19 +00:00
* /
2016-09-30 16:33:38 +00:00
} ( ) ) ;