Add support for ini file for server properties via --config

This commit is contained in:
Jansen Price 2020-08-31 01:04:44 -05:00
parent 19ff2d52a0
commit d8cb106997
8 changed files with 387 additions and 95 deletions

View File

@ -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 <path/to/cert/file> -text -noout

View File

@ -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();

44
config/example.ini Normal file
View File

@ -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

44
config/localhost.ini Normal file
View File

@ -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

199
src/Orbit/Cert.php Normal file
View File

@ -0,0 +1,199 @@
<?php
namespace Orbit;
use Monolog\Logger;
class Cert
{
public $hostname = '';
public $tls_certfile = '';
public $tls_keyfile = '';
public $key_passphrase = '';
public function __construct(Config $config, Logger $logger)
{
$this->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 = <<<EOS
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = $san_domains_string
[ usr_cert ]
basicConstraints=CA:FALSE
nsComment = "Generated Certificate by php unicorn"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
subjectAltName = $san_domains_string
EOS;
$temp_filename = tempnam("/tmp", "orbit-sslconf-");
file_put_contents($temp_filename, $str);
return $temp_filename;
}
private function ensureDirExists($filename)
{
$dir = dirname($filename);
if (!is_dir($dir)) {
// Recursively make directory if necessary
mkdir($dir, 0777, true);
}
}
}

View File

@ -7,16 +7,56 @@ class Config
public $host = "0.0.0.0";
public $port = 1965;
public $hostname = "localhost";
public $log_file = "./orbit.log";
public $tls_certfile = "./server.cert.pem";
public $tls_keyfile = "./server.key.pem";
public $tls_certfile = "";
public $tls_keyfile = "";
public $key_passphrase = "";
public $quiet = false;
public $verbose = false;
public $log_file = ".log/orbit.log";
public $log_level = "info";
public $root_dir = ".";
public $index_file = "index.gmi";
public $enable_directory_index = true;
private $is_development_server = false;
public function __construct($is_development = false)
{
$this->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;
}
}
}

View File

@ -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);

View File

@ -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);