Refactor into Orbit namespace

This commit is contained in:
Jansen Price 2020-08-26 17:57:01 -05:00
parent fc00fdbd63
commit 1a48642baa
7 changed files with 556 additions and 100 deletions

122
bin/orbit
View file

@ -1,105 +1,27 @@
#!/usr/bin/env php
<?php
// Certificate data:
$dn = array(
"countryName" => "UK",
"stateOrProvinceName" => "Somerset",
"localityName" => "Glastonbury",
"organizationName" => "The Brain Room Limited",
"organizationalUnitName" => "PHP Documentation Team",
"commonName" => "Wez Furlong",
"emailAddress" => "wez@example.com"
require_once dirname(__DIR__) . "/vendor/autoload.php";
// Define command line args for this client
// And fetch args passed in by invocation
$args = new \Qi_Console_ArgV(
$argv,
array(
'host|h:' => 'Set host/ip address to listen on (default 0.0.0.0)',
'port|p:' => 'Set port to listen on (default 1965)',
'pem-cert:' => 'Set cert PEM file to use (default ./server.pem)',
'log:' => 'Set log filename (default orbit.log)',
'help|h' => 'Show help',
'verbose|v' => 'Include more verbose output',
'quiet|q' => 'Print less messages',
'no-color' => 'Don\'t use color output',
'version' => 'Show version and exit',
)
);
$terminal = new \Qi_Console_Terminal();
$error_handler = new \Qi_Console_ExceptionHandler($terminal, true);
$console = new \Orbit\Console($args, $terminal);
// Generate certificate
$privkey = openssl_pkey_new();
$cert = openssl_csr_new($dn, $privkey);
$cert = openssl_csr_sign($cert, null, $privkey, 365);
// Generate PEM file
# Optionally change the passphrase from 'comet' to whatever you want, or leave it empty for no passphrase
$pem_passphrase = 'comet';
$pem = array();
openssl_x509_export($cert, $pem[0]);
openssl_pkey_export($privkey, $pem[1], $pem_passphrase);
$pem = implode($pem);
// Save PEM file
$pemfile = './server.pem';
file_put_contents($pemfile, $pem);
$context = stream_context_create();
// local_cert must be in PEM format
stream_context_set_option($context, 'ssl', 'local_cert', $pemfile);
// Pass Phrase (password) of private key
stream_context_set_option($context, 'ssl', 'passphrase', $pem_passphrase);
stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
stream_context_set_option($context, 'ssl', 'verify_peer', false);
// Create the server socket
$server = stream_socket_server('ssl://0.0.0.0:1965', $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context);
while (true) {
//$buffer = '';
print "waiting...";
$client = @stream_socket_accept($server);
if ($client) {
print "accepted " . stream_socket_get_name($client, true) . "\n";
$request = stream_get_line($client, 1024, "\r\n");
// Read until CRLF
//while(!preg_match('/\r\n/', $buffer) )
// $buffer .= fread($client, 2046);
print(View::hexView($request));
// Respond to client
$response = implode("\r\n", ['20 text/gemini', 'Thanks for all the fish']);
fwrite($client, $response);
fclose($client);
} else {
print "error.\n";
}
}
class View
{
/**
* View hex chars of string
*
* Outputs a listing of hexidecimal values in 16 byte rows
*
* @param mixed $text Input text
* @return string
*/
public static function hexView($text)
{
$num = 16;
$outStr = '';
$printableChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
. 'abcdefghijklmnopqrstuvwxyz'
. '0123456789~!@#$%^&*()_+-={}|[]\:";\'<>?,./';
$charCount = strlen($text);
for ($i = 0; $i < $charCount; $i += $num) {
$printStr = '';
for ($j = 0; $j < $num; $j++) {
$char = substr($text, $i+$j, 1);
$outStr .= sprintf("%02X", ord($char)) . " ";
if (ord($char) >= 32 && ord($char) < 127) {
$printStr .= $char;
} else {
$printStr .= ".";
}
}
$outStr .= " | " . $printStr . "\n";
}
return $outStr;
}
}
$value = $console->execute();
exit($value);

20
composer.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "sumpygump/orbit",
"description": "Server for gemini protocol",
"license": "MIT",
"authors": [
{
"name": "Jansen Price",
"email": "sumpygump@tilde.tam"
}
],
"require": {
"sumpygump/qi-console": "^1.3",
"monolog/monolog": "^2.1"
},
"autoload": {
"psr-4" : {
"Orbit\\" : "src/Orbit"
}
}
}

202
composer.lock generated Normal file
View file

@ -0,0 +1,202 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f2158d38eb2be637af2512ebe8b515b2",
"packages": [
{
"name": "monolog/monolog",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/f9eee5cec93dfb313a38b6b288741e84e53f02d5",
"reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5",
"shasum": ""
},
"require": {
"php": ">=7.2",
"psr/log": "^1.0.1"
},
"provide": {
"psr/log-implementation": "1.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^6.0",
"graylog2/gelf-php": "^1.4.2",
"php-amqplib/php-amqplib": "~2.4",
"php-console/php-console": "^3.1.3",
"php-parallel-lint/php-parallel-lint": "^1.0",
"phpspec/prophecy": "^1.6.1",
"phpunit/phpunit": "^8.5",
"predis/predis": "^1.1",
"rollbar/rollbar": "^1.3",
"ruflin/elastica": ">=0.90 <3.0",
"swiftmailer/swiftmailer": "^5.3|^6.0"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"php-console/php-console": "Allow sending log messages to Google Chrome",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "http://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2020-07-23T08:41:23+00:00"
},
{
"name": "psr/log",
"version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2020-03-23T09:12:05+00:00"
},
{
"name": "sumpygump/qi-console",
"version": "1.3.3",
"source": {
"type": "git",
"url": "https://github.com/sumpygump/qi-console.git",
"reference": "f3afecb5fe238799a09f38082379a3cd9057eb07"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sumpygump/qi-console/zipball/f3afecb5fe238799a09f38082379a3cd9057eb07",
"reference": "f3afecb5fe238799a09f38082379a3cd9057eb07",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "7.5.*"
},
"type": "library",
"autoload": {
"psr-0": {
"Qi_Console_": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jansen Price",
"email": "jansen.price@gmail.com"
}
],
"description": "Console utilities",
"homepage": "http://quantalideas.com/code/qi-console",
"keywords": [
"console",
"terminal",
"terminfo"
],
"time": "2020-08-26T20:40:44+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "1.1.0"
}

13
src/Orbit/Config.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace Orbit;
class Config
{
public $host = "0.0.0.0";
public $port = 1965;
public $log_file = "./orbit.log";
public $quiet = false;
public $verbose = false;
}

119
src/Orbit/Console.php Normal file
View file

@ -0,0 +1,119 @@
<?php
namespace Orbit;
use Monolog\Logger;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
class Console extends \Qi_Console_Client
{
public function execute()
{
if ($this->_args->version) {
$this->showVersion();
return 0;
}
if ($this->_args->help) {
$this->showHelp();
return 0;
}
$config = new Config();
if ($this->_args->host) {
$config->host = $this->_args->host;
}
if ($this->_args->port) {
$config->port = $this->_args->port;
}
if ($this->_args->log) {
$config->log_file = $this->_args->log;
}
if ($this->_args->quiet) {
$config->quiet = $this->_args->quiet;
}
if ($this->_args->verbose) {
$config->verbose = $this->_args->verbose;
}
if (!$config->quiet) {
print "Orbit // Gemini server software\n";
}
$server = new Server($config);
$server->setLogger($this->makeLogger($config));
$server->listen();
}
public function makeLogger($config)
{
$pid = getmypid();
$output = "[%datetime%] $pid %channel%.%level_name%: %message% %context%\n";
$formatter = new LineFormatter($output, 'Y-m-d\TH:i:s');
$logger = new Logger('orbit');
$level = Logger::INFO;
if ($config->verbose) {
$level = Logger::DEBUG;
}
$log_stream = new StreamHandler($config->log_file, $level);
$log_stream->setFormatter($formatter);
$logger->pushHandler($log_stream);
if (!$config->quiet) {
$std_stream = new StreamHandler('php://stdout', $level);
$std_stream->setFormatter($formatter);
$logger->pushHandler($std_stream);
}
return $logger;
}
public function showVersion()
{
print "Orbit " . Server::$version . "\n";
}
public function showHelp()
{
$this->showVersion();
$out = $this->_terminal->do_op()
. "Usage: orbit [options]\n"
. "\n"
. $this->_terminal->do_setaf(3)
. "Options:\n"
. $this->_terminal->do_op();
foreach ($this->_args->getHelp() as $key => $value) {
if (strpos($key, "|")) {
$parts = explode("|", $key);
$left = sprintf("-%s, --%s", $parts[0], $parts[1]);
} else {
$left = sprintf(" --%s", $key);
}
if ($left[-1] == ":") {
$left = substr($left, 0, -1) . " <arg>";
}
$out .= sprintf(
" %s%s %s%s\n",
$this->_terminal->do_setaf(2),
str_pad($left, 20),
$this->_terminal->do_op(),
$value,
);
}
print $out;
}
}

26
src/Orbit/Request.php Normal file
View file

@ -0,0 +1,26 @@
<?php
namespace Orbit;
class Request
{
public $url = '';
public $scheme;
public $host;
public $port;
public $user;
public $pass;
public $path;
public $query;
public $fragment;
public function __construct($request_input)
{
$this->url = $request_input;
$data = parse_url($request_input);
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
}

154
src/Orbit/Server.php Normal file
View file

@ -0,0 +1,154 @@
<?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;
}
}