2018-05-23 11:12:39 +00:00
#!/usr/bin/env node
( function ( ) {
'use strict' ;
var pkg = require ( '../package.json' ) ;
var argv = process . argv . slice ( 2 ) ;
2018-06-06 09:44:35 +00:00
var relay = require ( '../' ) ;
2018-05-24 19:19:50 +00:00
var Greenlock = require ( 'greenlock' ) ;
2018-05-23 11:12:39 +00:00
var confIndex = argv . indexOf ( '--config' ) ;
var confpath ;
if ( - 1 === confIndex ) {
confIndex = argv . indexOf ( '-c' ) ;
}
confpath = argv [ confIndex + 1 ] ;
function help ( ) {
console . info ( '' ) ;
console . info ( 'Usage:' ) ;
console . info ( '' ) ;
2018-06-11 17:24:57 +00:00
console . info ( '\ttelebit-relay --config <path>' ) ;
2018-05-23 11:12:39 +00:00
console . info ( '' ) ;
console . info ( 'Example:' ) ;
console . info ( '' ) ;
2018-06-11 17:24:57 +00:00
console . info ( '\ttelebit-relay --config /opt/telebit-relay/etc/telebit-relay.yml' ) ;
2018-05-23 11:12:39 +00:00
console . info ( '' ) ;
console . info ( 'Config:' ) ;
console . info ( '' ) ;
2018-06-11 17:24:57 +00:00
console . info ( '\tSee https://git.coolaj86.com/coolaj86/telebit-relay.js' ) ;
2018-05-23 11:12:39 +00:00
console . info ( '' ) ;
console . info ( '' ) ;
process . exit ( 0 ) ;
}
if ( - 1 === confIndex || - 1 !== argv . indexOf ( '-h' ) || - 1 !== argv . indexOf ( '--help' ) ) {
help ( ) ;
}
if ( ! confpath || /^--/ . test ( confpath ) ) {
help ( ) ;
}
function applyConfig ( config ) {
2018-06-06 06:59:03 +00:00
var state = { defaults : { } , ports : [ 80 , 443 ] , tcp : { } } ;
if ( 'undefined' !== typeof Promise ) {
state . Promise = Promise ;
} else {
state . Promise = require ( 'bluebird' ) ;
}
2018-05-23 11:12:39 +00:00
state . tlsOptions = { } ; // TODO just close the sockets that would use this early? or use the admin servername
state . config = config ;
state . servernames = config . servernames || [ ] ;
state . secret = state . config . secret ;
2018-05-25 01:13:05 +00:00
if ( ! state . secret ) {
2018-05-23 11:12:39 +00:00
state . secret = require ( 'crypto' ) . randomBytes ( 16 ) . toString ( 'hex' ) ;
console . info ( "" ) ;
console . info ( "Secret for this session:" ) ;
console . info ( "" ) ;
console . info ( "\t" + state . secret ) ;
console . info ( "" ) ;
console . info ( "" ) ;
}
2018-05-25 01:13:05 +00:00
if ( ! state . config . greenlock ) {
state . config . greenlock = { } ;
}
if ( ! state . config . greenlock . configDir ) {
state . config . greenlock . configDir = require ( 'os' ) . homedir ( ) + require ( 'path' ) . sep + 'acme' ;
}
2018-05-23 11:12:39 +00:00
function approveDomains ( opts , certs , cb ) {
2018-06-01 06:41:32 +00:00
if ( state . debug ) { console . log ( '[debug] approveDomains' , opts . domains ) ; }
2018-05-23 11:12:39 +00:00
// This is where you check your database and associated
// email addresses with domains and agreements and such
// The domains being approved for the first time are listed in opts.domains
// Certs being renewed are listed in certs.altnames
if ( certs ) {
opts . domains = certs . altnames ;
2018-05-24 19:19:50 +00:00
cb ( null , { options : opts , certs : certs } ) ;
return ;
}
2018-06-01 06:41:32 +00:00
if ( ! state . validHosts ) { state . validHosts = { } ; }
if ( ! state . validHosts [ opts . domains [ 0 ] ] && state . config . vhost ) {
if ( state . debug ) { console . log ( '[sni] vhost checking is turned on' ) ; }
2018-05-24 19:19:50 +00:00
var vhost = state . config . vhost . replace ( /:hostname/ , opts . domains [ 0 ] ) ;
require ( 'fs' ) . readdir ( vhost , function ( err , nodes ) {
2018-06-01 06:41:32 +00:00
if ( state . debug ) { console . log ( '[sni] checking fs vhost' , opts . domains [ 0 ] , ! err ) ; }
2018-06-11 17:24:57 +00:00
if ( err ) { check ( ) ; return ; }
2018-05-24 19:19:50 +00:00
if ( nodes ) { approve ( ) ; }
} ) ;
return ;
2018-05-23 11:12:39 +00:00
}
2018-05-24 19:19:50 +00:00
function approve ( ) {
2018-06-01 06:41:32 +00:00
state . validHosts [ opts . domains [ 0 ] ] = true ;
2018-05-24 19:19:50 +00:00
opts . email = state . config . email ;
opts . agreeTos = state . config . agreeTos ;
2018-06-01 06:41:32 +00:00
opts . communityMember = state . config . communityMember || state . config . greenlock . communityMember ;
2018-05-24 19:19:50 +00:00
opts . challenges = {
// TODO dns-01
'http-01' : require ( 'le-challenge-fs' ) . create ( { webrootPath : '/tmp/acme-challenges' } )
} ;
opts . communityMember = state . config . communityMember ;
cb ( null , { options : opts , certs : certs } ) ;
}
2018-05-23 11:12:39 +00:00
2018-05-24 19:19:50 +00:00
function check ( ) {
2018-06-01 06:41:32 +00:00
if ( state . debug ) { console . log ( '[sni] checking servername' ) ; }
2018-05-24 19:19:50 +00:00
if ( - 1 !== state . servernames . indexOf ( opts . domain ) || - 1 !== ( state . _servernames || [ ] ) . indexOf ( opts . domain ) ) {
approve ( ) ;
} else {
cb ( new Error ( "failed the approval chain '" + opts . domains [ 0 ] + "'" ) ) ;
}
}
2018-05-23 11:12:39 +00:00
2018-06-01 06:41:32 +00:00
check ( ) ;
2018-05-23 11:12:39 +00:00
}
2018-05-24 19:19:50 +00:00
state . greenlock = Greenlock . create ( {
2018-05-23 11:12:39 +00:00
2018-05-26 08:07:49 +00:00
version : state . config . greenlock . version || 'draft-11'
, server : state . config . greenlock . server || 'https://acme-v02.api.letsencrypt.org/directory'
2018-05-23 11:12:39 +00:00
2018-06-01 06:41:32 +00:00
, store : require ( 'le-store-certbot' ) . create ( { debug : state . config . debug || state . config . greenlock . debug , webrootPath : '/tmp/acme-challenges' } )
2018-05-23 11:12:39 +00:00
, approveDomains : approveDomains
2018-06-01 06:41:32 +00:00
, telemetry : state . config . telemetry || state . config . greenlock . telemetry
2018-05-26 08:07:49 +00:00
, configDir : state . config . greenlock . configDir
2018-05-26 08:48:23 +00:00
, debug : state . config . debug || state . config . greenlock . debug
2018-05-23 11:12:39 +00:00
} ) ;
2018-05-24 19:19:50 +00:00
2018-06-06 10:56:38 +00:00
try {
// TODO specify extensions in config file
state . extensions = require ( '../lib/extensions' ) ;
} catch ( e ) {
2018-06-15 08:46:43 +00:00
if ( 'ENOENT' !== e . code || state . debug ) { console . log ( '[DEBUG] no extensions loaded' , e ) ; }
2018-06-06 10:56:38 +00:00
state . extensions = { } ;
}
2018-06-06 06:59:03 +00:00
require ( '../lib/handlers' ) . create ( state ) ; // adds directly to config for now...
2018-05-24 19:19:50 +00:00
//require('cluster-store').create().then(function (store) {
//program.store = store;
2018-06-06 10:56:38 +00:00
2018-06-06 06:59:03 +00:00
state . authenticate = function ( opts ) {
2018-06-06 10:56:38 +00:00
if ( state . extensions . authenticate ) {
try {
return state . extensions . authenticate ( {
state : state
, auth : opts . auth
} ) ;
} catch ( e ) {
console . error ( 'Extension Error:' ) ;
console . error ( e ) ;
}
2018-06-06 06:59:03 +00:00
}
return state . defaults . authenticate ( opts . auth ) ;
} ;
// default authenticator for single-user setup
// (i.e. personal use on DO, Vultr, or RPi)
state . defaults . authenticate = function onAuthenticate ( jwtoken ) {
return state . Promise . resolve ( ) . then ( function ( ) {
var jwt = require ( 'jsonwebtoken' ) ;
var auth ;
var token ;
var decoded ;
try {
token = jwt . verify ( jwtoken , state . secret ) ;
} catch ( e ) {
token = null ;
}
return token ;
} ) ;
} ;
2018-05-24 19:19:50 +00:00
var net = require ( 'net' ) ;
2018-06-06 09:44:35 +00:00
var netConnHandlers = relay . create ( state ) ; // { tcp, ws }
2018-05-24 19:19:50 +00:00
var WebSocketServer = require ( 'ws' ) . Server ;
var wss = new WebSocketServer ( { server : ( state . httpTunnelServer || state . httpServer ) } ) ;
wss . on ( 'connection' , netConnHandlers . ws ) ;
state . ports . forEach ( function ( port ) {
if ( state . tcp [ port ] ) {
2018-06-01 06:41:32 +00:00
console . warn ( "[cli] skipping previously added port " + port ) ;
2018-05-24 19:19:50 +00:00
return ;
}
state . tcp [ port ] = net . createServer ( ) ;
state . tcp [ port ] . listen ( port , function ( ) {
2018-06-01 06:41:32 +00:00
console . info ( '[cli] Listening for TCP connections on' , port ) ;
2018-05-24 19:19:50 +00:00
} ) ;
2018-05-31 06:18:02 +00:00
state . tcp [ port ] . on ( 'connection' , netConnHandlers . tcp ) ;
2018-05-24 19:19:50 +00:00
} ) ;
//});
2018-05-23 11:12:39 +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 ;
}
}
applyConfig ( camelCopy ( config ) ) ;
} ) ;
function adjustArgs ( ) {
function collectServernames ( val , memo ) {
var lowerCase = val . split ( /,/ ) . map ( function ( servername ) {
return servername . toLowerCase ( ) ;
} ) ;
return memo . concat ( lowerCase ) ;
}
function collectProxies ( val , memo ) {
var vals = val . split ( /,/g ) ;
vals . map ( function ( location ) {
// http:john.example.com:3000
// http://john.example.com:3000
var parts = location . split ( ':' ) ;
if ( 1 === parts . length ) {
parts [ 1 ] = parts [ 0 ] ;
parts [ 0 ] = 'wss' ;
}
if ( 2 === parts . length ) {
if ( /\./ . test ( parts [ 0 ] ) ) {
parts [ 2 ] = parts [ 1 ] ;
parts [ 1 ] = parts [ 0 ] ;
parts [ 0 ] = 'wss' ;
}
if ( ! /\./ . test ( parts [ 1 ] ) ) {
throw new Error ( "bad --serve option Example: wss://tunnel.example.com:1337" ) ;
}
}
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 ( - 1 !== [ 'ws' , 'http' ] . indexOf ( parts [ 0 ] ) ) {
//parts[2] = 80;
}
else if ( - 1 !== [ 'wss' , 'https' ] . indexOf ( parts [ 0 ] ) ) {
//parts[2] = 443;
}
else {
throw new Error ( "port must be specified - ex: tls:*:1337" ) ;
}
}
return {
protocol : parts [ 0 ]
, hostname : parts [ 1 ]
, port : parts [ 2 ]
} ;
} ) . forEach ( function ( val ) {
memo . push ( val ) ;
} ) ;
return memo ;
}
function collectPorts ( val , memo ) {
return memo . concat ( val . split ( /,/g ) . map ( Number ) . filter ( Boolean ) ) ;
}
program
. version ( pkg . version )
. option ( '--agree-tos' , "Accept the Daplie and Let's Encrypt Terms of Service" )
. option ( '--email <EMAIL>' , "Email to use for Daplie and Let's Encrypt accounts" )
. option ( '--serve <URL>' , 'comma separated list of <proto>:<//><servername>:<port> to which matching incoming http and https should forward (reverse proxy). Ex: https://john.example.com,tls:*:1337' , collectProxies , [ ] )
. option ( '--ports <PORT>' , 'comma separated list of ports on which to listen. Ex: 80,443,1337' , collectPorts , [ ] )
. option ( '--servernames <STRING>' , 'comma separated list of servernames to use for the admin interface. Ex: tunnel.example.com,tunnel.example.net' , collectServernames , [ ] )
2018-06-11 17:24:57 +00:00
. option ( '--secret <STRING>' , 'the same secret used by telebit-relay (used for JWT authentication)' )
2018-05-23 11:12:39 +00:00
. parse ( process . argv )
;
var portsMap = { } ;
var servernamesMap = { } ;
program . serve . forEach ( function ( proxy ) {
servernamesMap [ proxy . hostname ] = true ;
if ( proxy . port ) {
portsMap [ proxy . port ] = true ;
}
} ) ;
program . servernames . forEach ( function ( name ) {
servernamesMap [ name ] = true ;
} ) ;
program . ports . forEach ( function ( port ) {
portsMap [ port ] = true ;
} ) ;
program . servernames = Object . keys ( servernamesMap ) ;
if ( ! program . servernames . length ) {
throw new Error ( 'You must give this server at least one servername for its admin interface. Example:\n\n\t--servernames tunnel.example.com,tunnel.example.net' ) ;
}
program . ports = Object . keys ( portsMap ) ;
if ( ! program . ports . length ) {
program . ports = [ 80 , 443 ] ;
}
if ( ! program . secret ) {
// TODO randomly generate and store in file?
console . warn ( "[SECURITY] you must provide --secret '" + require ( 'crypto' ) . randomBytes ( 16 ) . toString ( 'hex' ) + "'" ) ;
process . exit ( 1 ) ;
return ;
}
//program.tlsOptions.SNICallback = program.greenlock.httpsOptions.SNICallback;
/ *
program . middleware = program . greenlock . middleware ( function ( req , res ) {
res . end ( 'Hello, World!' ) ;
} ) ;
* /
}
//adjustArgs();
} ( ) ) ;