orbit/src/Orbit/Server.php
2020-08-26 17:57:01 -05:00

155 lines
4.3 KiB
PHP

<?php
namespace Orbit;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class Server
{
public static $version = "0.2";
public $config;
public $pemfile = "./server.pem";
public $pem_passphrase = "";
public $timeout = 60;
private $ssl_context;
private $logger;
public function __construct(Config $config = null)
{
if ($config == null) {
$this->config = new Config();
} else {
$this->config = $config;
}
$this->timeout = ini_get("default_socket_timeout");
if (file_exists($this->pemfile)) {
$this->log(Logger::DEBUG, "Using existing cert.");
} else {
$this->log(Logger::DEBUG, "Generating new cert.");
$this->generateCert();
}
$this->ssl_context = $this->createSslContext();
}
public function setLogger(Logger $logger)
{
$this->logger = $logger;
}
public function getLogger()
{
if (!$this->logger) {
$this->logger = new Logger('orbit');
$this->logger->pushHandler(new StreamHandler($this->config->log_file, Logger::INFO));
}
return $this->logger;
}
public function listen()
{
$server = stream_socket_server(
$this->getListenAddress(),
$errno, $errstr,
STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,
$this->ssl_context
);
if (!$server) {
throw new \Exception("Error " . $errno . ": " . $errstr);
}
$protocol = "gemini";
$name = stream_socket_get_name($server, false);
$this->log(Logger::NOTICE, "Listening on $protocol://$name ...");
while (true) {
# This is to swallow up the `timeout` warning
set_error_handler([$this, 'onWarning']);
$client = stream_socket_accept($server, $this->timeout, $client_name);
restore_error_handler();
if ($client) {
$this->log(Logger::DEBUG, "$client_name Accepted");
$request_buffer = stream_get_line($client, 1024, "\r\n");
$this->log(Logger::INFO, "REQ: $request_buffer", ["client" => $client_name]);
$request = new Request($request_buffer);
// Respond to client
$response = implode("\r\n", ['20 text/gemini', "Thanks for all the fish\n"]);
fwrite($client, $response);
fclose($client);
$this->log(Logger::DEBUG, "$client_name Closed");
}
}
}
public function log($level, $message, $context = [])
{
$this->getLogger()->log($level, $message, $context);
}
public function onWarning($id, $message)
{
if (strpos($message, "Operation timed out") !== false) {
// Do nothing
return;
}
// Something else happened.
throw new \Exception($id . ' ' . $message);
}
public function getListenAddress()
{
return sprintf('tls://%s:%s', $this->config->host, $this->config->port);
}
private function generateCert()
{
// Certificate data
$dn = [
"countryName" => "UK",
"stateOrProvinceName" => "X",
"localityName" => "X",
"organizationName" => "X",
"organizationalUnitName" => "X",
"commonName" => "orbit1.0",
"emailAddress" => "X"
];
// Generate certificate
$privkey = openssl_pkey_new();
$cert = openssl_csr_new($dn, $privkey);
$cert = openssl_csr_sign($cert, null, $privkey, 365);
// Generate PEM file
$pem = [];
openssl_x509_export($cert, $pem[0]);
openssl_pkey_export($privkey, $pem[1], $this->pem_passphrase);
$pem = implode($pem);
// Save PEM file
file_put_contents($this->pemfile, $pem);
}
public function createSslContext()
{
$context = stream_context_create();
// local_cert must be in PEM format
stream_context_set_option($context, 'ssl', 'local_cert', $this->pemfile);
stream_context_set_option($context, 'ssl', 'passphrase', $this->pem_passphrase);
stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
stream_context_set_option($context, 'ssl', 'verify_peer', false);
return $context;
}
}