#!/usr/bin/env node // // cli entrypoint for starting a Configurable Proxy // // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. // "use strict"; var fs = require("fs"), pkg = require("../package.json"), cli = require("commander"), tls = require("tls"), winston = require("winston"); cli .version(pkg.version) .option("--ip ", "Public-facing IP of the proxy") .option("--port (defaults to 8000)", "Public-facing port of the proxy", parseInt) .option("--ssl-key ", "SSL key to use, if any") .option("--ssl-cert ", "SSL certificate to use, if any") .option("--ssl-ca ", "SSL certificate authority, if any") .option("--ssl-request-cert", "Request SSL certs to authenticate clients") .option( "--ssl-reject-unauthorized", "Reject unauthorized SSL connections (only meaningful if --ssl-request-cert is given)" ) .option("--ssl-protocol ", "Set specific SSL protocol, e.g. TLSv1_2, SSLv3") .option("--ssl-ciphers ", "`:`-separated ssl cipher list. Default excludes RC4") .option("--ssl-allow-rc4", "Allow RC4 cipher for SSL (disabled by default)") .option("--ssl-dhparam ", "SSL Diffie-Helman Parameters pem file, if any") .option("--api-ip ", "Inward-facing IP for API requests", "localhost") .option( "--api-port ", "Inward-facing port for API requests (defaults to --port=value+1)", parseInt ) .option("--api-ssl-key ", "SSL key to use, if any, for API requests") .option("--api-ssl-cert ", "SSL certificate to use, if any, for API requests") .option("--api-ssl-ca ", "SSL certificate authority, if any, for API requests") .option("--api-ssl-request-cert", "Request SSL certs to authenticate clients for API requests") .option( "--api-ssl-reject-unauthorized", "Reject unauthorized SSL connections (only meaningful if --api-ssl-request-cert is given)" ) .option("--client-ssl-key ", "SSL key to use, if any, for proxy to client requests") .option( "--client-ssl-cert ", "SSL certificate to use, if any, for proxy to client requests" ) .option( "--client-ssl-ca ", "SSL certificate authority, if any, for proxy to client requests" ) .option("--client-ssl-request-cert", "Request SSL certs to authenticate clients for API requests") .option( "--client-ssl-reject-unauthorized", "Reject unauthorized SSL connections (only meaningful if --client-ssl-request-cert is given)" ) .option("--default-target ", "Default proxy target (proto://host[:port])") .option( "--error-target ", "Alternate server for handling proxy errors (proto://host[:port])" ) .option("--error-path ", "Alternate server for handling proxy errors (proto://host[:port])") .option( "--redirect-port ", "Redirect HTTP requests on this port to the server on HTTPS" ) .option("--redirect-to ", "Redirect HTTP requests from --redirect-port to this port") .option("--pid-file ", "Write our PID to a file") // passthrough http-proxy options .option("--no-x-forward", "Don't add 'X-forward-' headers to proxied requests") .option("--no-prepend-path", "Avoid prepending target paths to proxied requests") .option("--no-include-prefix", "Don't include the routing prefix in proxied requests") .option("--auto-rewrite", "Rewrite the Location header host/port in redirect responses") .option("--change-origin", "Changes the origin of the host header to the target URL") .option( "--protocol-rewrite ", "Rewrite the Location header protocol in redirect responses to the specified protocol" ) .option( "--custom-header
", "Custom header to add to proxied requests. Use same option for multiple headers (--custom-header k1:v1 --custom-header k2:v2)", collectHeadersIntoObject, {} ) .option("--insecure", "Disable SSL cert verification") .option("--host-routing", "Use host routing (host as first level of path)") .option("--metrics-ip ", "IP for metrics server", "") .option("--metrics-port ", "Port of metrics server. Defaults to no metrics server") .option("--log-level ", "Log level (debug, info, warn, error)", "info") .option( "--timeout ", "Timeout (in millis) when proxy drops connection for a request.", parseInt ) .option( "--proxy-timeout ", "Timeout (in millis) when proxy receives no response from target.", parseInt ) .option( "--storage-backend ", "Define an external storage class. Defaults to in-MemoryStore." ) .option( "--keep-alive-timeout ", "Set timeout (in milliseconds) for Keep-Alive connections", parseInt ); // collects multiple flags to an object // --custom-header "k1:v1" --custom-header " k2 : v2 " --> {"k1":"v1","k2":"v2"} function collectHeadersIntoObject(value, previous) { value = value.trim(); var colon = value.indexOf(":"); if (colon <= 0) { console.error("A colon was expected in custom header: " + value); process.exit(1); } var headerName = value.slice(0, colon).trim(); var headerValue = value.slice(colon + 1).trim(); previous[headerName] = headerValue; return previous; } cli.parse(process.argv); var args = cli.opts(); var ConfigurableProxy = require("../lib/configproxy.js").ConfigurableProxy; var log = require("../lib/log.js").defaultLogger({ level: args.logLevel.toLowerCase() }); var options = { log: log }; var sslCiphers; if (args.sslCiphers) { sslCiphers = args.sslCiphers; } else { var rc4 = "!RC4"; // disable RC4 by default if (args.sslAllowRc4) { // autoCamelCase is duMb rc4 = "RC4"; } // ref: https://iojs.org/api/tls.html#tls_modifying_the_default_tls_cipher_suite sslCiphers = [ "ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384", "DHE-RSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-SHA256", "DHE-RSA-AES128-SHA256", "ECDHE-RSA-AES256-SHA384", "DHE-RSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA256", "DHE-RSA-AES256-SHA256", "HIGH", rc4, "!aNULL", "!eNULL", "!EXPORT", "!DES", "!RC4", "!MD5", "!PSK", "!SRP", "!CAMELLIA", ].join(":"); } function _loadSslCa(caFile) { // When multiple CAs need to be specified, they must be broken into // an array of certs unfortunately. var chain = fs.readFileSync(caFile, "utf8"); var ca = []; var cert = []; chain.split("\n").forEach(function (line) { cert.push(line); if (line.match(/-END CERTIFICATE-/)) { ca.push(new Buffer(cert.join("\n"))); cert = []; } }); return ca; } // ssl options if (args.sslKey || args.sslCert) { options.ssl = {}; if (args.sslKey) { options.ssl.key = fs.readFileSync(args.sslKey); if (process.env.CONFIGPROXY_SSL_KEY_PASSPHRASE) { options.ssl.passphrase = process.env.CONFIGPROXY_SSL_KEY_PASSPHRASE; } } if (args.sslCert) { options.ssl.cert = fs.readFileSync(args.sslCert); } if (args.sslCa) { options.ssl.ca = fs.readFileSync(args.sslCa); } if (args.sslDhparam) { options.ssl.dhparam = fs.readFileSync(args.sslDhparam); } if (args.sslProtocol) { options.ssl.secureProtocol = args.sslProtocol + "_method"; } options.ssl.ciphers = sslCiphers; options.ssl.honorCipherOrder = true; options.ssl.requestCert = args.sslRequestCert; options.ssl.rejectUnauthorized = args.sslRejectUnauthorized; } // ssl options for the API interface if (args.apiSslKey || args.apiSslCert) { options.apiSsl = {}; if (args.apiSslKey) { options.apiSsl.key = fs.readFileSync(args.apiSslKey); if (process.env.CONFIGPROXY_API_SSL_KEY_PASSPHRASE) { options.apiSsl.passphrase = process.env.CONFIGPROXY_API_SSL_KEY_PASSPHRASE; } } if (args.apiSslCert) { options.apiSsl.cert = fs.readFileSync(args.apiSslCert); } if (args.apiSslCa) { options.apiSsl.ca = _loadSslCa(args.apiSslCa); } if (args.sslDhparam) { options.apiSsl.dhparam = fs.readFileSync(args.sslDhparam); } if (args.sslProtocol) { options.apiSsl.secureProtocol = args.sslProtocol + "_method"; } options.apiSsl.ciphers = sslCiphers; options.apiSsl.honorCipherOrder = true; options.apiSsl.requestCert = args.apiSslRequestCert; options.apiSsl.rejectUnauthorized = args.apiSslRejectUnauthorized; } if (args.clientSslKey || args.clientSslCert || args.clientSslCa) { options.clientSsl = {}; if (args.clientSslKey) { options.clientSsl.key = fs.readFileSync(args.clientSslKey); } if (args.clientSslCert) { options.clientSsl.cert = fs.readFileSync(args.clientSslCert); } if (args.clientSslCa) { options.clientSsl.ca = _loadSslCa(args.clientSslCa); } if (args.sslDhparam) { options.clientSsl.dhparam = fs.readFileSync(args.sslDhparam); } if (args.sslProtocol) { options.clientSsl.secureProtocol = args.sslProtocol + "_method"; } options.clientSsl.ciphers = sslCiphers; options.clientSsl.honorCipherOrder = true; options.clientSsl.requestCert = args.clientSslRequestCert; options.clientSsl.rejectUnauthorized = args.clientSslRejectUnauthorized; } // because camelCase is the js way! options.defaultTarget = args.defaultTarget; options.errorTarget = args.errorTarget; options.errorPath = args.errorPath; options.hostRouting = args.hostRouting; options.authToken = process.env.CONFIGPROXY_AUTH_TOKEN; options.redirectPort = args.redirectPort; options.redirectTo = args.redirectTo; options.headers = args.customHeader; options.timeout = args.timeout; options.proxyTimeout = args.proxyTimeout; options.keepAliveTimeout = args.keepAliveTimeout; // metrics options options.enableMetrics = !!args.metricsPort; // certs need to be provided for https redirection if (!options.ssl && options.redirectPort) { log.error("HTTPS redirection specified but certificates not provided."); process.exit(1); } if (options.errorTarget && options.errorPath) { log.error("Cannot specify both error-target and error-path. Pick one."); process.exit(1); } // passthrough for http-proxy options if (args.insecure) options.secure = false; options.xfwd = args.xForward; options.prependPath = args.prependPath; options.includePrefix = args.includePrefix; if (args.autoRewrite) { options.autoRewrite = true; log.info("AutoRewrite of Location headers enabled."); } if (args.changeOrigin) { options.changeOrigin = true; log.info("Change Origin of host headers enabled."); } if (args.protocolRewrite) { options.protocolRewrite = args.protocolRewrite; log.info("ProtocolRewrite enabled. Rewriting to " + options.protocolRewrite); } if (!options.authToken) { log.warn("REST API is not authenticated."); } // external backend class options.storageBackend = args.storageBackend; var proxy = new ConfigurableProxy(options); var listen = {}; listen.port = parseInt(args.port) || 8000; if (args.ip === "*") { // handle ip=* alias for all interfaces log.warn( "Interpreting ip='*' as all-interfaces. Preferred usage is 0.0.0.0 for all IPv4 or '' for all-interfaces." ); args.ip = ""; } listen.ip = args.ip; listen.apiIp = args.apiIp; listen.apiPort = args.apiPort || listen.port + 1; listen.metricsIp = args.metricsIp; listen.metricsPort = args.metricsPort; proxy.proxyServer.listen(listen.port, listen.ip); proxy.apiServer.listen(listen.apiPort, listen.apiIp); if (listen.metricsPort) { proxy.metricsServer.listen(listen.metricsPort, listen.metricsIp); } log.info( "Proxying %s://%s:%s to %s", options.ssl ? "https" : "http", listen.ip || "*", listen.port, options.defaultTarget || "(no default)" ); log.info( "Proxy API at %s://%s:%s/api/routes", options.apiSsl ? "https" : "http", listen.apiIp || "*", listen.apiPort ); if (listen.metricsPort) { log.info("Serve metrics at %s://%s:%s/metrics", "http", listen.metricsIp, listen.metricsPort); } if (args.pidFile) { log.info("Writing pid %s to %s", process.pid, args.pidFile); var fd = fs.openSync(args.pidFile, "w"); fs.writeSync(fd, process.pid.toString()); fs.closeSync(fd); process.on("exit", function () { log.debug("Removing %s", args.pidFile); fs.unlinkSync(args.pidFile); }); } // Redirect HTTP to HTTPS on the proxy's port if (options.redirectPort && listen.port !== 80) { var http = require("http"); var redirectPort = options.redirectTo ? options.redirectTo : listen.port; var server = http .createServer(function (req, res) { if (typeof req.headers.host === "undefined") { res.statusCode = 400; res.write( "This server is HTTPS-only on port " + redirectPort + ", but an HTTP request was made and the host could not be determined from the request." ); res.end(); return; } var host = req.headers.host.split(":")[0]; // Make sure that when we redirect, it's to the port the proxy is running on // or the port to which we have been instructed to forward. if (redirectPort !== 443) { host = host + ":" + redirectPort; } res.writeHead(301, { Location: "https://" + host + req.url }); res.end(); }) .listen(options.redirectPort, () => { log.info( "Added HTTP to HTTPS redirection from " + server.address().port + " to " + redirectPort ); }); } // trigger normal exit on SIGINT // without this, PID cleanup won't fire on SIGINT process.on("SIGINT", function () { log.warn("Interrupted"); process.exit(2); }); // trigger normal exit on SIGTERM // fired on `docker stop` and during Kubernetes pod container evictions process.on("SIGTERM", function () { log.warn("Terminated"); process.exit(2); }); // log uncaught exceptions, don't exit now that setup is complete process.on("uncaughtException", function (e) { log.error("Uncaught Exception: " + e.message); if (e.stack) { log.error(e.stack); } });