From d8cb106997d7d7cf8f0ee759f5b9d75c4aad1b7b Mon Sep 17 00:00:00 2001 From: Jansen Price Date: Mon, 31 Aug 2020 01:04:44 -0500 Subject: [PATCH] Add support for ini file for server properties via --config --- bin/makecert | 14 ++- bin/orbit | 6 +- config/example.ini | 44 ++++++++++ config/localhost.ini | 44 ++++++++++ src/Orbit/Cert.php | 199 ++++++++++++++++++++++++++++++++++++++++++ src/Orbit/Config.php | 50 +++++++++-- src/Orbit/Console.php | 34 ++++---- src/Orbit/Server.php | 91 +++++++------------ 8 files changed, 387 insertions(+), 95 deletions(-) create mode 100644 config/example.ini create mode 100644 config/localhost.ini create mode 100644 src/Orbit/Cert.php diff --git a/bin/makecert b/bin/makecert index f48c253..7306122 100755 --- a/bin/makecert +++ b/bin/makecert @@ -20,18 +20,14 @@ if [ -z "$HOSTNAME" ]; then fi echo "Making cert and key for host '$HOSTNAME'" +mkdir -vp certs +# This one will generate a cert with SANs suitable for local dev openssl req -x509 -newkey rsa:4096 -nodes\ -days 365 -subj "/CN=$HOSTNAME"\ - -keyout "$HOSTNAME.key.pem"\ - -out "$HOSTNAME.cert.pem" - -# Use this one below with the -addext to include multiple domains (e.g. subdomains) -#openssl req -x509 -newkey rsa:4096 -nodes\ -# -days 365 -subj "/CN=$HOSTNAME"\ -# -keyout "$HOSTNAME.key.pem"\ -# -out "$HOSTNAME.cert.pem" -# -addext "subjectAltName=DNS:example.com,DNS:www.example.net,IP:10.0.0.1" + -keyout "certs/$HOSTNAME.key.pem"\ + -out "certs/$HOSTNAME.cert.pem"\ + -addext "subjectAltName=DNS:$HOSTNAME,IP:127.0.0.1,IP:0.0.0.0" # To inspect a cert use the following command #openssl x509 -in -text -noout diff --git a/bin/orbit b/bin/orbit index fd7c5d4..e8bb951 100755 --- a/bin/orbit +++ b/bin/orbit @@ -31,13 +31,15 @@ $autoload = require_once ORBIT_COMPOSER_AUTOLOAD; $args = new \Qi_Console_ArgV( $argv, [ + 'config|c:' => 'Use specified config file (.ini) for configuration', 'host:' => 'Set host/ip address to listen on (default 0.0.0.0)', 'port|p:' => 'Set port to listen on (default 1965)', - 'hostname:' => 'Set hostname of server', + 'hostname:' => 'Set hostname of server (default localhost)', 'tls-cert:' => 'Set cert PEM file to use (default null)', 'tls-key:' => 'Set private key PEM file to use (default null)', 'root-dir:' => 'Set the file root directory', 'log:' => 'Set log filename (default orbit.log)', + 'dev' => 'Allow developer server functions (default false)', 'help|h' => 'Show help', 'verbose|v' => 'Include more verbose output', 'quiet|q' => 'Print less messages', @@ -46,7 +48,7 @@ $args = new \Qi_Console_ArgV( ] ); $terminal = new \Qi_Console_Terminal(); -$error_handler = new \Qi_Console_ExceptionHandler($terminal, true); +$error_handler = new \Qi_Console_ExceptionHandler($terminal, true, true); $console = new \Orbit\Console($args, $terminal); $value = $console->execute(); diff --git a/config/example.ini b/config/example.ini new file mode 100644 index 0000000..13ed3ea --- /dev/null +++ b/config/example.ini @@ -0,0 +1,44 @@ +; Example Orbit config file + +; Host : The IP address designation that orbit should listen on +host = "0.0.0.0" + +; Port : the port that orbit should listen on +port = 1965 + +; Hostname : the expected domain that orbit is serving +hostname = "example.com" + +; TLS cert file : The location to the cert file +tls_certfile = "/etc/orbit/example.com.cert.pem" + +; TLS key file : The location to the private key for the server +tls_keyfile = "/etc/orbit/example.com.key.pem" + +; Key passphrase : optional passphrase for server key +key_passphrase = "" + +; Log file : where orbit should log information +log_file = "/var/log/orbit/orbit.log" + +; Log level : Only log messages that are at or above this level +; Possible values (from low to high): +; - debug +; - info +; - notice +; - warning +; - error +; - critical +; - alert +; - emergency +log_level = "info" + +; Root dir : Root directory of the server files +root_dir = "/var/gemini" + +; Index file : Name of the index file (when a directory is accessed) +index_file = "index.gmi" + +; Enable directory index : Whether orbit should serve up a directory listing +; for directories accessed that have no index file present +enable_directory_index = true diff --git a/config/localhost.ini b/config/localhost.ini new file mode 100644 index 0000000..91077e0 --- /dev/null +++ b/config/localhost.ini @@ -0,0 +1,44 @@ +; Example Orbit config file for localhost + +; Host : The IP address designation that orbit should listen on +host = "0.0.0.0" + +; Port : the port that orbit should listen on +port = 1965 + +; Hostname : the expected domain that orbit is serving +hostname = "localhost" + +; TLS cert file : The location to the cert file +tls_certfile = "certs/localhost.cert.pem" + +; TLS key file : The location to the private key for the server +tls_keyfile = "certs/localhost.key.pem" + +; Key passphrase : optional passphrase for server key +key_passphrase = "" + +; Log file : where orbit should log information +log_file = "log/orbit.log" + +; Log level : Only log messages that are at or above this level +; Possible values (from low to high): +; - debug +; - info +; - notice +; - warning +; - error +; - critical +; - alert +; - emergency +log_level = "info" + +; Root dir : Root directory of the server files +root_dir = "." + +; Index file : Name of the index file (when a directory is accessed) +index_file = "index.gmi" + +; Enable directory index : Whether orbit should serve up a directory listing +; for directories accessed that have no index file present +enable_directory_index = true diff --git a/src/Orbit/Cert.php b/src/Orbit/Cert.php new file mode 100644 index 0000000..3791d72 --- /dev/null +++ b/src/Orbit/Cert.php @@ -0,0 +1,199 @@ +hostname = $config->hostname; + $this->tls_certfile = $config->tls_certfile; + $this->tls_keyfile = $config->tls_keyfile; + $this->key_passphrase = $config->key_passphrase; + + $this->logger = $logger; + + if ($config->getIsDevelopmentServer()) { + $this->initDevelopment(); + } else { + $this->initProduction(); + } + } + + /** + * Initialize for development mode + * + * If the cert files do not exist, generate a new self-signed cert + * + * @return bool + */ + public function initDevelopment() + { + $this->logger->debug("Initialize cert for development mode."); + + if ($this->tls_certfile == '') { + $this->tls_certfile = sprintf("certs/%s.cert.pem", $this->hostname); + } + + if ($this->tls_keyfile == '') { + $this->tls_keyfile = sprintf("certs/%s.key.pem", $this->hostname); + } + + if (file_exists($this->tls_certfile) && file_exists($this->tls_keyfile)) { + $this->logger->info(sprintf("Using existing cert file '%s'", $this->tls_certfile)); + $this->logger->info(sprintf("Using existing key file '%s'", $this->tls_keyfile)); + } else { + $this->logger->info(sprintf("Generating new cert file '%s'", $this->tls_certfile)); + $this->logger->info(sprintf("Generating new key file '%s'", $this->tls_keyfile)); + if (file_exists($this->tls_certfile)) { + $this->logger->warning(sprintf("Warning! May overwrite existing cert file '%s'", $this->tls_certfile)); + } + if (file_exists($this->tls_keyfile)) { + $this->logger->warning(sprintf("Warning! May overwrite existing key file '%s'", $this->tls_keyfile)); + } + $this->generateCert(); + } + + return true; + } + + /** + * Initialize for production mode + * + * Cert/key files must be provided. If fails, generates exception + * + * @return bool + */ + public function initProduction() + { + $this->logger->debug("Initialize cert for production mode."); + + $errors = []; + if ($this->tls_certfile == '') { + $errors[] = "Missing required cert file: use --tls-cert to specify;"; + } + + if ($this->tls_keyfile == '') { + $errors[] = "Missing required key file: use --tls-key to specify;"; + } + + if ($this->tls_certfile && !file_exists($this->tls_certfile)) { + $errors[] = sprintf("Cert file '%s' does not exist or is not readable!", $this->tls_certfile); + } + if ($this->tls_keyfile && !file_exists($this->tls_keyfile)) { + $errors[] = sprintf("Key file '%s' does not exist or is not readable!", $this->tls_keyfile); + } + + if (count($errors)) { + $errors[] = "\nTry running with --dev to generate a self-signed cert automatically."; + $this->logger->alert(implode("\n", $errors)); + throw new \Exception("\n" . implode("\n", $errors)); + } + + $this->logger->debug(sprintf("Using cert file '%s'", $this->tls_certfile)); + $this->logger->debug(sprintf("Using key file '%s'", $this->tls_keyfile)); + return true; + } + + private function generateCert() + { + // Certificate data + $dn = [ + "countryName" => "UK", + "stateOrProvinceName" => "X", + "localityName" => "X", + "organizationName" => "X", + "organizationalUnitName" => "X", + "commonName" => $this->hostname, + "emailAddress" => "X", + ]; + + $days_valid = 365; + $san_domains = ["DNS:" . $this->hostname, "IP:127.0.0.1", "IP:0.0.0.0"]; + $ssl_config = $this->createOpenSslConf($san_domains); + + $csr_config = ['digest_alg' => 'sha256', 'req_extensions' => 'v3_req', 'config' => $ssl_config]; + $cert_config = ['digest_alg' => 'sha256', 'x509_extensions' => 'usr_cert', 'config' => $ssl_config]; + + // Generate certificate + $private_key = openssl_pkey_new([ + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + 'private_key_bits' => 2048 + ]); + $cert = openssl_csr_new($dn, $private_key, $csr_config); + $cert = openssl_csr_sign($cert, null, $private_key, $days_valid, $cert_config); + + // Generate PEM files + $pem = []; + openssl_x509_export($cert, $pem[0]); + openssl_pkey_export($private_key, $pem[1], $this->key_passphrase); + + // Ensure dir exists for cert files + $this->ensureDirExists($this->tls_certfile); + $this->ensureDirExists($this->tls_keyfile); + + // Save PEM files + file_put_contents($this->tls_certfile, $pem[0]); + file_put_contents($this->tls_keyfile, $pem[1]); + + // Remove temp sslconf file + unlink($ssl_config); + } + + /** + * createOpenSslConf + * + * Creating this temp file to be used in the CSR and the cert signing is + * required to add csr request and x509 extensions into the cert in order + * to include the subject alternative names + * + * @param array $san_domains + * @return string Filename + */ + private function createOpenSslConf($san_domains = []) + { + $san_domains_string = implode(",", $san_domains); + + $str = <<setIsDevelopmentServer($is_development); + } + + public function setIsDevelopmentServer($is_development_server) + { + $this->is_development_server = (bool) $is_development_server; + return $this; + } + + public function getIsDevelopmentServer() + { + return $this->is_development_server; + } + + public function readFromIniFile($filename) + { + if (!file_exists($filename) || !is_readable($filename)) { + throw new \Exception("Cannot read config file '$filename'"); + } + + $ini = parse_ini_file($filename); + + $valid_keys = [ + 'host', 'port', 'hostname', 'tls_certfile', + 'tls_keyfile', 'keypassphrase', 'log_file', 'log_level', + 'root_dir', 'index_file', 'enable_directory_index' + ]; + + foreach ($ini as $key => $value) { + if (!in_array($key, $valid_keys)) { + continue; + } + + $this->{$key} = $value; + } + } } diff --git a/src/Orbit/Console.php b/src/Orbit/Console.php index 911500e..8a00e5f 100644 --- a/src/Orbit/Console.php +++ b/src/Orbit/Console.php @@ -25,19 +25,25 @@ class Console extends \Qi_Console_Client } $config = $this->makeConfig(); + $logger = $this->makeLogger($config, $this->_args->quiet); - if (!$config->quiet) { + if (!$this->_args->quiet) { print "Orbit // Gemini server software\n"; - print ":: Using cert file " . $config->tls_certfile . "\n"; - print ":: Using key file " . $config->tls_keyfile . "\n"; } - $server = new Server($config, $this->makeLogger($config)); + + $cert = new Cert($config, $logger); + + $server = new Server($config, $cert, $logger); $server->listen(); } public function makeConfig() { - $config = new Config(); + $config = new Config($this->_args->dev); + + if ($this->_args->config) { + $config->readFromIniFile($this->_args->config); + } if ($this->_args->host) { $config->host = $this->_args->host; @@ -55,12 +61,8 @@ class Console extends \Qi_Console_Client $config->log_file = $this->_args->log; } - if ($this->_args->quiet) { - $config->quiet = $this->_args->quiet; - } - if ($this->_args->verbose) { - $config->verbose = $this->_args->verbose; + $config->log_level = Logger::DEBUG; } if ($this->_args->get("root-dir")) { @@ -69,33 +71,29 @@ class Console extends \Qi_Console_Client if ($this->_args->get("tls-cert")) { $config->tls_certfile = $this->_args->get("tls-cert"); - } else { - $config->tls_certfile = sprintf("%s.cert.pem", $config->hostname); } if ($this->_args->get("tls-key")) { $config->tls_keyfile = $this->_args->get("tls-key"); - } else { - $config->tls_keyfile = sprintf("%s.key.pem", $config->hostname); } return $config; } - public function makeLogger($config) + public function makeLogger($config, $is_quiet = false) { $logger = new Logger('orbit'); $level = Logger::INFO; - if ($config->verbose) { - $level = Logger::DEBUG; + if ($config->log_level) { + $level = $config->log_level; } $log_stream = new StreamHandler($config->log_file, $level); $log_stream->setFormatter($this->makeLogFormatter()); $logger->pushHandler($log_stream); - if (!$config->quiet) { + if (!$is_quiet) { $std_stream = new StreamHandler('php://stdout', $level); $std_stream->setFormatter($this->makeLogFormatter(true)); $logger->pushHandler($std_stream); diff --git a/src/Orbit/Server.php b/src/Orbit/Server.php index 7436646..bebf350 100644 --- a/src/Orbit/Server.php +++ b/src/Orbit/Server.php @@ -10,15 +10,13 @@ class Server public static $version = "0.2"; public $config; - public $tls_certfile = "./server.cert.pem"; - public $tls_keyfile = "./server.key.pem"; - public $key_passphrase = ""; + public $cert; public $timeout = 60; private $ssl_context; private $logger; - public function __construct(Config $config = null, Logger $logger = null) + public function __construct(Config $config = null, Cert $cert = null, Logger $logger = null) { if ($config == null) { $this->config = new Config(); @@ -28,22 +26,18 @@ class Server if ($logger !== null) { $this->setLogger($logger); + } else { + $this->getLogger(); + } + + if ($cert == null) { + $this->cert = new Cert($config, $this->getLogger()); + } else { + $this->cert = $cert; } $this->timeout = ini_get("default_socket_timeout"); - if (file_exists($this->config->tls_certfile) && file_exists($this->config->tls_keyfile)) { - $this->log(Logger::DEBUG, "Using existing cert + key."); - } else { - $this->log(Logger::DEBUG, "Generating new cert."); - if (file_exists($this->config->tls_certfile)) { - $this->log(Logger::WARNING, "Warning! May overwrite existing cert file '" . $this->config->tls_certfile . "'\n"); - } - if (file_exists($this->config->tls_keyfile)) { - $this->log(Logger::WARNING, "Warning! May overwrite existing key file '" . $this->config->tls_keyfile . "'\n"); - } - $this->generateCert(); - } $this->ssl_context = $this->createSslContext(); } @@ -62,15 +56,19 @@ class Server return $this->logger; } - public function listen($root_dir = ".") + public function listen($root_dir = null) { - $path = realpath($root_dir); - - if (!is_dir($path)) { - throw new \Exception("Error: Root directory '$path' not a directory"); + if (null == $root_dir) { + $root_dir = $this->config->root_dir; } - $this->log(Logger::DEBUG, "Root directory '$path'"); + if (!is_dir($root_dir)) { + throw new \Exception("Error: Root directory '$root_dir' not a directory"); + } + + $path = realpath($root_dir); + + $this->logger->debug("Root directory '$path'"); $server = stream_socket_server( $this->getListenAddress(), @@ -85,7 +83,7 @@ class Server $protocol = "gemini"; $name = stream_socket_get_name($server, false); - $this->log(Logger::INFO, "Listening on $protocol://$name ..."); + $this->logger->info("Listening on $protocol://$name ..."); while (true) { # This is to swallow up the `timeout` warning @@ -96,23 +94,22 @@ class Server if ($client) { $time = ['start' => microtime(true)]; - $this->log(Logger::DEBUG, "$client_name Accepted"); + $this->logger->debug("$client_name Accepted"); $request_buffer = stream_get_line($client, 1024, "\r\n"); - $this->log(Logger::INFO, "REQ: $request_buffer", ["client" => $client_name]); + $this->logger->info("REQ: $request_buffer", ["client" => $client_name]); $request = new Request($request_buffer); // Respond to client $response = $this->handleResponse($request, $path); $size = $response->send($client); $time['end'] = microtime(true); - $this->log( - Logger::DEBUG, + $this->logger->debug( "RSP: " . trim($response->getHeader()), ['size' => $size, 'time' => $time['end'] - $time['start']] ); fclose($client); - $this->log(Logger::DEBUG, "$client_name Closed"); + $this->logger->debug("$client_name Closed"); } } } @@ -186,10 +183,10 @@ class Server // Do nothing return; } - $error = sprintf("Error %s: %s", $id, $message); - $this->log(Logger::ERROR, "Connection Closed"); // Something else happened. + $error = sprintf("Error %s: %s", $id, $message); + $this->logger->error($error); throw new \Exception($error); } @@ -229,42 +226,14 @@ class Server return $body; } - private function generateCert() - { - // Certificate data - $dn = [ - "countryName" => "UK", - "stateOrProvinceName" => "X", - "localityName" => "X", - "organizationName" => "X", - "organizationalUnitName" => "X", - "commonName" => $this->config->hostname, - "emailAddress" => "X" - ]; - - // Generate certificate - $privkey = openssl_pkey_new(); - $cert = openssl_csr_new($dn, $privkey); - $cert = openssl_csr_sign($cert, null, $privkey, 365); - - // Generate PEM files - $pem = []; - openssl_x509_export($cert, $pem[0]); - openssl_pkey_export($privkey, $pem[1], $this->config->key_passphrase); - - // Save PEM files - file_put_contents($this->config->tls_certfile, $pem[0]); - file_put_contents($this->config->tls_keyfile, $pem[1]); - } - public function createSslContext() { $context = stream_context_create(); // local_cert must be in PEM format - stream_context_set_option($context, 'ssl', 'local_cert', $this->config->tls_certfile); - stream_context_set_option($context, 'ssl', 'local_pk', $this->config->tls_keyfile); - stream_context_set_option($context, 'ssl', 'passphrase', $this->config->key_passphrase); + stream_context_set_option($context, 'ssl', 'local_cert', $this->cert->tls_certfile); + stream_context_set_option($context, 'ssl', 'local_pk', $this->cert->tls_keyfile); + stream_context_set_option($context, 'ssl', 'passphrase', $this->cert->key_passphrase); stream_context_set_option($context, 'ssl', 'allow_self_signed', true); stream_context_set_option($context, 'ssl', 'verify_peer', false);