Compare commits

...

13 Commits
0.4 ... master

Author SHA1 Message Date
Jansen Price 3ec4797183 Update README with php extension dependencies 2020-10-03 23:51:56 -05:00
Jansen Price a33fc84627 Add PHP version dep in composer file 2020-10-03 23:51:41 -05:00
Jansen Price 33ab4eecdc Correct some order of operations in make install 2020-10-03 23:51:27 -05:00
Jansen Price 15cd024b35 Correct tests for version check 2020-09-29 12:41:48 -05:00
Jansen Price ec7a27af1d Add installation and usage details to README 2020-09-28 14:06:12 -05:00
Jansen Price 151299ea06 Correct typo in makefile help 2020-09-28 09:56:59 -05:00
Jansen Price e7b35e0023 Correct tests for renamed property 2020-09-28 09:43:08 -05:00
Jansen Price 2991248c47 Correctly ingest key passphrase for server key 2020-09-28 09:35:13 -05:00
Jansen Price 5ce094362e Update makefile with better explanation 2020-09-11 02:30:40 -05:00
Jansen Price b6c8e590de Add Makefile to install as systemctl service 2020-09-11 02:14:22 -05:00
Jansen Price 5eb44de02d Handle correctly symlinked files in server content files 2020-09-09 01:37:24 -05:00
Jansen Price 24bcbcb281 Minor upgrade in one dependency (lock file) 2020-09-09 01:31:56 -05:00
Jansen Price a1c3d730ca Clean up during testing, add module interface 2020-09-07 17:05:38 -05:00
19 changed files with 708 additions and 64 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
vendor
*.log
*.pem
.phpunit.result.cache

78
Makefile Normal file
View File

@ -0,0 +1,78 @@
ccyellow = $(shell echo "\033[33m")
ccend = $(shell tput op)
notice = $(ccyellow) $(0) $(ccend)
_path := $(shell pwd)
_user := gemini-data
ORBIT_HOSTNAME ?= localhost
SERVICE_PATH = /etc/systemd/system/orbit.service
EXEC_PATH = /usr/local/bin/orbit
CONF_PATH = /etc/orbit
LOG_PATH = /var/log/orbit
help:
@echo "Orbit Gemini protocol server"
@echo "To run tests, run 'make test'"
@echo "To install as a service on your system run:"
@echo " 'sudo make install ORBIT_HOSTNAME=localhost'"
@echo " But replace localhost with your desired hostname"
test:
cd tests; ../vendor/bin/phpunit .
install_bin:
@echo
@echo "$(ccyellow)> Installing orbit bin$(ccend)"
rm -f $(EXEC_PATH)
ln -s $(_path)/bin/orbit $(EXEC_PATH)
install_conf:
@echo
@echo "$(ccyellow)> Installing orbit config; Hostname: $(ORBIT_HOSTNAME)$(ccend)"
install -d $(CONF_PATH)
install config/localhost.ini $(CONF_PATH)/config.ini
sed -i -e 's#Example##' $(CONF_PATH)/config.ini
sed -i -e 's#localhost#$(ORBIT_HOSTNAME)#' $(CONF_PATH)/config.ini
sed -i -e 's#certs#$(CONF_PATH)/certs#' $(CONF_PATH)/config.ini
sed -i -e 's#log/#$(LOG_PATH)/#' $(CONF_PATH)/config.ini
install -o $(_user) -d $(LOG_PATH)
install_cert:
ifeq (,$(wildcard $(CONF_PATH)/certs/$(ORBIT_HOSTNAME).cert.pem))
@echo
@echo "$(ccyellow)> Generating certs for host $(ORBIT_HOSTNAME)$(ccend)"
bin/makecert $(ORBIT_HOSTNAME)
install -d $(CONF_PATH)/certs
install certs/$(ORBIT_HOSTNAME).cert.pem $(CONF_PATH)/certs/$(ORBIT_HOSTNAME).cert.pem
install certs/$(ORBIT_HOSTNAME).key.pem $(CONF_PATH)/certs/$(ORBIT_HOSTNAME).key.pem
rm -rf certs
endif
install_user:
ifeq ($(shell id -u $(_user) 2>/dev/null),)
@echo
@echo "$(ccyellow)> Creating user $(_user)$(ccend)"
useradd -m -d /var/gemini -s /usr/sbin/nologin -c "Orbit gemini service account" -U $(_user)
endif
install_service:
@echo
@echo "$(ccyellow)> Creating orbit.service file at $(SERVICE_PATH)$(ccend)"
install orbit.service $(SERVICE_PATH)
sed -i -e 's/{USER}/$(_user)/' $(SERVICE_PATH)
install: install_bin install_user install_conf install_cert install_service
@echo
@echo "--------------------------------"
@echo "done"
@echo "> The service runs with the user $(_user)"
@echo "> The actual orbit script is at $(EXEC_PATH)"
@echo " - But that is just a symlink to $(_path)/bin/orbit"
@echo "> I created a config file at $(CONF_PATH)/config.ini"
@echo " - Edit settings in that file to change configuration settings"
@echo "> I created a self-signed cert at $(CONF_PATH)/certs"
@echo "> The server log will be written at $(LOG_PATH)/orbit.log"
@echo "Run 'sudo systemctl start orbit' to start the service"
@echo "Run 'sudo systemctl restart orbit' to restart the service"
@echo "Run 'sudo systemctl enable orbit' to configure orbit to auto-start when machine starts"

230
README.md
View File

@ -1,3 +1,229 @@
# orbit
# 🛰️ Orbit
Server for gemini protocol written in PHP
Server for gemini protocol written in PHP
## Dependencies
- PHP 7.x or higher
- composer (see getcomposer.org for download/install instructions)
For dev dependencies:
- php-xml
- php-mbstring
You can install PHP + required dev extensions on debian-based linux with
```
sudo apt install php-cli php-xml php-mbstring
```
## Installation
Clone this repository to a suitable location.
```
git clone https://tildegit.org/sumpygump/orbit.git
```
Fetch composer dependencies.
```
cd orbit
composer update
```
You can choose to do `composer update --no-dev` to not include the dev dependencies (to run unit tests).
## Setup
After installation, you can invoke the service with `bin/orbit`. You should pass in some important configuration options. See the configuration section below for more details.
### Quickstart
For a quickstart, you can do the following:
```
sudo mkdir /var/gemini
sudo sh -c 'echo "# Hello world\n\nServer is running" > /var/gemini/index.gmi'
bin/orbit -c config/localhost.ini --dev
```
This will create a new directory to be the root of your gemini server at `/var/gemini`. Then we create an example index file at the root of the server. Then we run orbit pointing to the config file at `config/localhost.ini`. Running orbit with the `--dev` flag will automatically create the server key and cert files.
### Install as service
To install as a service on your GNU/Linux machine, take a look at the `orbit.service` file in this repository.
An accompanied Makefile in this repository will assist you in setting up as a service (only works on GNU/Linux). Change "localhost" in the command below to whatever the hostname for your server should be.
```
sudo make install ORBIT_HOSTNAME=localhost
```
This will perform the following steps:
- Symlink the orbit bin script at `/usr/local/bin/orbit`
- Create config file at `/etc/orbit/config.ini`
- Create self-signed cert for you at `/etc/orbit/certs`
- Create a new non-login user `gemini-data` for the service to use
- Create service file at `/etc/systemd/system/orbit.service`
Once this completes successfully, you may edit the config file to your liking, or install your own cert files. The default location of the server root is `/var/gemini`.
To start the service run `sudo systemctl start orbit` or `sudo service orbit start`
## Usage
Run `bin/orbit --help` to get some helpful command line invocation options.
```
$ bin/orbit --help
Orbit 0.4
Usage: orbit [options]
Options:
-c, --config <arg> Use specified config file (.ini) for configuration
--host <arg> Set host/ip address to listen on (default 0.0.0.0)
-p, --port <arg> Set port to listen on (default 1965)
--hostname <arg> Set hostname of server (default localhost)
--tls-cert <arg> Set cert PEM file to use (default null)
--tls-key <arg> Set private key PEM file to use (default null)
--tls-passphrase <arg> Set passphrase for private key
--root-dir <arg> Set the file root directory
--log <arg> Set log filename (default orbit.log)
--dev Allow developer server functions (default false)
-h, --help Show help
-v, --verbose Include more verbose output
-q, --quiet Print less messages
--no-color Don't use color output
--version Show version and exit
```
## Configuration
Most configuration options are available to be set at the invocation of orbit. The option `-c` allows you to define a path to a config file to define any configuration options not specified as command arguments.
```
; 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
```
### Host
The host config option is the IP address the server should bind to. Examples include 0.0.0.0 (to listen on all ipv4 addresses), or [::] to listen on all ipv6 addresses.
In a config file, the option is specified with `host = "0.0.0.0"`. From the command line arguments, the option is specified with `--host 0.0.0.0`
### Port
The port config option is which port the server will listen for incoming connections. The default for gemini is 1965, but orbit can be bound to any port that is available.
If the port is not available, you will see the following error message "Exception: Error 0: Address already in use"
In a config file, the option is specified with `port = 1965`. In command line arguments, the option is specified with `--port 1965`
### Hostname
The hostname config option tells orbit which hostname the server is serving. It will deny requests that do match this domain.
Config file option: `hostname = localhost`. Command line arg: `--hostname localhost`
### TLS cert file
The TLS cert file config option allows you to define the path to the cert file in PEM format.
Config file option: `tls_certfile = "/path/to/cert.pem"`. Command line arg: `--tls-cert /path/to/cert.pem`.
### TLS key file
The TLS key file config option allows you to define the path to the private key file for the server in PEM format.
Config file option: `tls_keyfile = "/path/to/key.pem"`. Command line arg: `--tls-key /path/to/key.pem`.
### Key passphrase
If the private key requires a passphrase, you can specifiy it in the config option.
Config file option: `key_passphrase = "secret"`. Command line arg: `--tls-passphrase secret`.
### Log file
The log file config option allows you to define the location where the orbit log file should write to. It must be writable by the user used to launch orbit.
Config file option: `log_file = "/path/to/orbit.log"`. Command line arg: `--log path/to/orbit.log`.
### Log level
The log level config option allows you to specify the log level of the messages written to stdout or the log file. The levels are debug, info, notice, warning, error, critical, alert, emergency. Only log messages at or above the level specified will be include.
Config file option: `log_level = "info"`.
It is not possible to set granular levels with the command line arguments. The default is 'info'. If you use the `-v` or `--verbose` option, it will set the log level to 'debug'.
### Root directory
The root directory config option allows you to specify the path that is the root directory of the server.
Config file option: `root_dir = "/var/gemini"`. Command line arg: `--root-dir /var/gemini`.
### Index file
If a request is made for a directory, orbit will look in the directory for the index file and serve it. Define what the name of this file will be by setting the index file config option. The default is "index.gmi".
Config file option: `index_file = "index.gmi"`.
### Enable directory index
If a request is made for a directory and there is no index file in the target directory, orbit can optionally return a list of files/directories within that directory. To turn this on, set the enable directory index config option. The default is to for this to be enabled.
Config file option: `enable_directory_index = true`.
### `--dev` option
The `--dev` command line argument flag tells orbit that this is a development server environment. When this is enabled, orbit will automatically create a self-signed cert at runtime.
### `-q / --quiet` option
The `--quiet` command line argument flag tells orbit to not output anything to stdout.

View File

@ -37,6 +37,7 @@ $args = new \Qi_Console_ArgV(
'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)',
'tls-passphrase:' => 'Set passphrase for private key',
'root-dir:' => 'Set the file root directory',
'log:' => 'Set log filename (default orbit.log)',
'dev' => 'Allow developer server functions (default false)',

View File

@ -9,6 +9,7 @@
}
],
"require": {
"php": ">=7.2",
"sumpygump/qi-console": "^1.3",
"monolog/monolog": "^2.1",
"neitanod/forceutf8": "^2.0"
@ -16,6 +17,9 @@
"require-dev": {
"phpunit/phpunit": "^9.3"
},
"bin": [
"bin/orbit"
],
"autoload": {
"psr-4" : {
"Orbit\\" : "src/Orbit"

10
composer.lock generated
View File

@ -721,16 +721,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.1.7",
"version": "9.1.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2"
"reference": "f98f8466126d83b55b924a94d2244c53c216b8fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2ef92bec3186a827faf7362ff92ae4e8ec2e49d2",
"reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f98f8466126d83b55b924a94d2244c53c216b8fb",
"reference": "f98f8466126d83b55b924a94d2244c53c216b8fb",
"shasum": ""
},
"require": {
@ -790,7 +790,7 @@
"type": "github"
}
],
"time": "2020-09-03T07:09:19+00:00"
"time": "2020-09-07T08:07:10+00:00"
},
{
"name": "phpunit/php-file-iterator",

View File

@ -34,7 +34,7 @@ log_file = "log/orbit.log"
log_level = "info"
; Root dir : Root directory of the server files
root_dir = "."
root_dir = "/var/gemini"
; Index file : Name of the index file (when a directory is accessed)
index_file = "index.gmi"

14
orbit.service Normal file
View File

@ -0,0 +1,14 @@
[Unit]
Description=Orbit gemini server
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User={USER}
ExecStart=/usr/local/bin/orbit --config /etc/orbit/config.ini -q
[Install]
WantedBy=multi-user.target

View File

@ -53,21 +53,31 @@ class Config
* @param string $filename Path to ini file
* @return void
*/
public function readFromIniFile(string $filename)
public function readFromIniFile(string $filename): void
{
if (!file_exists($filename) || !is_readable($filename)) {
throw new \Exception("Cannot read config file '$filename'");
}
$ini = parse_ini_file($filename);
$this->readFromArray($ini);
}
/**
* Read config values from array
*
* @param array $data Array of config values
* @return void
*/
public function readFromArray(array $params): void
{
$valid_keys = [
'host', 'port', 'hostname', 'tls_certfile',
'tls_keyfile', 'keypassphrase', 'log_file', 'log_level',
'root_dir', 'index_file', 'enable_directory_index'
'tls_keyfile', 'key_passphrase', 'log_file', 'log_level',
'root_dir', 'index_file', 'enable_directory_index',
];
foreach ($ini as $key => $value) {
foreach ($params as $key => $value) {
if (!in_array($key, $valid_keys)) {
continue;
}

View File

@ -102,6 +102,10 @@ class Console extends \Qi_Console_Client
$config->tls_keyfile = $this->_args->get("tls-key");
}
if ($this->_args->get("tls-passphrase")) {
$config->key_passphrase = $this->_args->get("tls-passphrase");
}
return $config;
}

View File

@ -11,7 +11,7 @@ use Monolog\Logger;
*
* @package Orbit
*/
class Module
class Module implements ModuleInterface
{
protected $config;
protected $logger;
@ -28,4 +28,16 @@ class Module
$this->config = $config;
$this->logger = $logger;
}
/**
* Handle
*
* Take a request and return a response
*
* This should be implemented in a child class
*/
public function handle(Request $request): array
{
return [true, new Response()];
}
}

View File

@ -14,24 +14,26 @@ use Orbit\Response;
*/
class Statics extends Module
{
const WORLD_READABLE = 0x0004;
/**
* Handle a request and generate a proper response
*
* @param Request $request The request object
* @param Response $response The already created response object
* @param string $real_root_dir The real path to root on disk
*/
public function handleResponse(Request $request, Response $response, $real_root_dir): array
public function handle(Request $request): array
{
$response = new Response();
$real_root_dir = realpath($this->config->root_dir);
$resource_path = rtrim($real_root_dir, "/") . $request->path;
// Check if within the server root
// Realpath will translate any '..' in the path
$realpath = realpath($resource_path);
// getAbsolutePath will translate any '..' in the path
$realpath = self::getAbsolutePath($resource_path);
if ($realpath && strpos($realpath, $real_root_dir) !== 0) {
$response->setStatus(Response::STATUS_PERMANENT_FAILURE);
$response->setMeta("Invalid location");
return [true, $response];
return [false, $response];
}
if (is_dir($resource_path)) {
@ -39,7 +41,7 @@ class Statics extends Module
if ($resource_path[-1] != "/") {
$response->setStatus(Response::STATUS_REDIRECT_PERMANENT);
$response->setMeta($request->getUrlAppendPath('/'));
return [true, $response];
return [false, $response];
}
// Check if index file exists
@ -47,9 +49,9 @@ class Statics extends Module
$resource_path = $resource_path . DIRECTORY_SEPARATOR . $this->config->index_file;
} else {
if (!$this->config->enable_directory_index) {
$response->setStatus(Response::STATUS_BAD_REQUEST);
$response->setStatus(Response::STATUS_NOT_FOUND);
$response->setMeta('Path not available');
return [true, $response];
return [false, $response];
} else {
$response->setStatus(Response::STATUS_SUCCESS);
$response->setMeta('text/gemini');
@ -60,22 +62,21 @@ class Statics extends Module
}
// File exists and is world readable
if (file_exists($resource_path) && self::isWorldReadble($resource_path)) {
if (file_exists($resource_path) && self::isWorldReadable($resource_path)) {
$response->setStatus(Response::STATUS_SUCCESS);
$pathinfo = pathinfo($resource_path);
// TODO : handle files without extensions
if (!isset($pathinfo['extension'])) {
$response->setStatus(Response::STATUS_TEMPORARY_FAILURE);
$response->setMeta('Error reading resource');
return [true, $response];
if (isset($pathinfo['extension'])) {
$meta = $this->getCustomMimeFromFileExtension($pathinfo['extension']);
if (!$meta) {
$meta = mime_content_type($resource_path);
}
} else {
// Use finfo_file to detect type
$meta = finfo_file(finfo_open(FILEINFO_MIME), $resource_path);
}
$meta = $this->getCustomMimeFromFileExtension($pathinfo['extension']);
if (!$meta) {
$meta = mime_content_type($resource_path);
}
$response->setMeta($meta);
$response->setStaticFile($resource_path);
} else {
@ -123,8 +124,12 @@ class Statics extends Module
{
$files = glob($path . "*");
$body = "# Directory listing " . str_replace($root, '', $path) . "\n\n";
$body .= "=> " . str_replace($root, '', dirname($path)) . " ..\n";
$body = "# Directory listing " . str_replace($root, '', $path) . "/\n\n";
if ($path != $root . "/") {
// If not already at root, provide option to go up one parent
$body .= "=> " . str_replace($root, '', dirname($path)) . "/ ..\n";
}
foreach ($files as $file) {
$relative_path = str_replace($path, '', $file);
@ -149,4 +154,44 @@ class Statics extends Module
return $body;
}
/**
* Report whether a given file is world readable or not
*
* @param string $file The file to check
* @return bool
*/
public static function isWorldReadable(string $file): bool
{
return (bool)(fileperms($file) & self::WORLD_READABLE);
}
/**
* Get an absolute path for a filename
*
* Translates .. and . to the real locations. The reason I am not using
* realpath() to do it is it resolves symlinks
*
* @param string $path
* @return string
*/
public static function getAbsolutePath($path): string
{
$path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
$parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
$absolutes = [];
foreach ($parts as $part) {
if ('.' == $part) {
continue;
}
if ('..' == $part) {
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
return "/" . implode(DIRECTORY_SEPARATOR, $absolutes);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace Orbit;
use Monolog\Logger;
/**
* Module interface
*
* This represents some work that a request->response handler can do
*
* @package Orbit
*/
interface ModuleInterface
{
/**
* Handle
*
* Take a request and generate a response
*
* Must return an array with two values: [bool, Response];
* The first value (bool) must indicate whether the response should
* continue on or if it should be returned immediately to the client.
*/
public function handle(Request $request): array;
}

View File

@ -60,7 +60,7 @@ class Response
}
/**
* Send data to client
* Send response body to client
*
* @param resource $client
* @return int|false Number of bytes written
@ -152,11 +152,21 @@ class Response
* @param int $status
* @return void
*/
public function setStatus($status): void
public function setStatus(int $status): void
{
$this->status = $status;
}
/**
* Get status
*
* @return int
*/
public function getStatus(): int
{
return $this->status;
}
/**
* Set response meta value
*
@ -167,4 +177,14 @@ class Response
{
$this->meta = $meta;
}
/**
* Get meta
*
* @return string Meta value
*/
public function getMeta(): string
{
return $this->meta;
}
}

View File

@ -16,9 +16,8 @@ use Orbit\Module\Statics;
class Server
{
const SCHEME = "gemini";
const WORLD_READABLE = 0x0004;
public static $version = "0.4";
public static $version = "0.5";
public $config;
public $cert;
@ -29,7 +28,6 @@ class Server
private $connections = []; // Incoming client connections
private $peers = []; // Client connections to read from
private $metas = []; // Meta data for each client connection
private $real_root_dir;
/**
* Constructor
@ -84,21 +82,17 @@ class Server
/**
* Listen and handle incoming requests
*
* @param mixed $root_dir The root directory from which to serve files
* @return void
*/
public function listen($root_dir = null): void
public function listen(): void
{
if (null == $root_dir) {
$root_dir = $this->config->root_dir;
}
$root_dir = $this->config->root_dir;
if (!is_dir($root_dir)) {
throw new \Exception("Error: Root directory '$root_dir' not a directory");
}
$this->real_root_dir = realpath($root_dir);
$this->logger->debug(sprintf("Root directory '%s'", $this->real_root_dir));
$this->logger->debug(sprintf("Root directory '%s'", realpath($root_dir)));
$server = stream_socket_server(
$this->getListenAddress(),
@ -201,7 +195,7 @@ class Server
$request = new Request($request_buffer);
// Respond to client
$response = $this->handleResponse($request);
$response = $this->handle($request);
$size = 0;
try {
@ -229,7 +223,7 @@ class Server
* @param Request $request
* @return Response
*/
public function handleResponse(Request $request): Response
public function handle(Request $request): Response
{
[$is_valid, $response] = $this->validateRequest($request);
if ($is_valid === false) {
@ -239,22 +233,11 @@ class Server
}
$statics_module = new Statics($this->config, $this->logger);
[$success, $response] = $statics_module->handleResponse($request, $response, $this->real_root_dir);
[$success, $response] = $statics_module->handle($request);
return $response;
}
/**
* Report whether a given file is world readable or not
*
* @param string $file The file to check
* @return bool
*/
public static function isWorldReadble(string $file): bool
{
return (bool)(fileperms($file) & self::WORLD_READABLE);
}
/**
* Prune any lingering connections
*

View File

@ -57,7 +57,7 @@ port=1988
hostname=beatles.org
tls_certfile=1212
tls_keyfile=3434
keypassphrase=strawberry
key_passphrase=strawberry
log_file=xyz.log
log_level=cherry
root_dir=blueberry
@ -73,7 +73,7 @@ EOF;
$this->assertSame('beatles.org', $config->hostname);
$this->assertSame('1212', $config->tls_certfile);
$this->assertSame('3434', $config->tls_keyfile);
$this->assertSame('strawberry', $config->keypassphrase);
$this->assertSame('strawberry', $config->key_passphrase);
$this->assertSame('xyz.log', $config->log_file);
$this->assertSame('cherry', $config->log_level);
$this->assertSame('blueberry', $config->root_dir);

View File

@ -92,7 +92,7 @@ final class ConsoleTest extends TestCase
$output = ob_get_contents();
ob_get_clean();
$this->assertStringContainsString('Orbit 0.4', $output);
$this->assertStringContainsString('Orbit ', $output);
}
public function testExecuteHelp(): void
@ -104,7 +104,7 @@ final class ConsoleTest extends TestCase
$output = ob_get_contents();
ob_get_clean();
$this->assertStringContainsString('Orbit 0.4', $output);
$this->assertStringContainsString('Orbit ', $output);
}
public function testMakeConfigWithConfig(): void

View File

@ -0,0 +1,191 @@
<?php declare(strict_types=1);
namespace Orbit\Tests;
use PHPUnit\Framework\TestCase;
use Monolog\Logger;
use Orbit\Config;
use Orbit\Module\Statics;
use Orbit\Request;
use Orbit\Response;
final class StaticsTest extends TestCase
{
public function makeObject($config_params = []): Statics
{
$config = new Config();
$config->readFromArray($config_params);
$statics = new Statics($config, new Logger('test-orbit'));
return $statics;
}
public function testConstruct(): void
{
$statics = new Statics(new Config(), new Logger('test-orbit'));
$this->assertInstanceOf(Statics::class, $statics);
}
public function testHandle(): void
{
$statics = $this->makeObject();
$request = new Request('lmnop');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_NOT_FOUND, $response->getStatus());
$this->assertTrue($success);
}
public function testHandleFileAttemptAboveRoot(): void
{
$statics = $this->makeObject();
$request = new Request('/../README.md');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_PERMANENT_FAILURE, $response->getStatus());
$this->assertFalse($success);
}
public function testHandleDirectoryWithRedirect(): void
{
@mkdir('dir1');
$statics = $this->makeObject();
$request = new Request('/dir1');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_REDIRECT_PERMANENT, $response->getStatus());
$this->assertSame('gemini:///dir1/', $response->getMeta());
$this->assertFalse($success);
@rmdir('dir1');
}
public function testHandleDirectoryWithIndexFile(): void
{
@mkdir('dir1');
file_put_contents('dir1/index.gmi', '# Sunlit lands');
$statics = $this->makeObject();
$request = new Request('/dir1/');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_SUCCESS, $response->getStatus());
$this->assertSame('# Sunlit lands', $response->getBody());
$this->assertTrue($success);
unlink('dir1/index.gmi');
@rmdir('dir1');
}
public function testHandleNoDirectoryIndex(): void
{
@mkdir('dir1');
$statics = $this->makeObject(['enable_directory_index' => false]);
$request = new Request('/dir1/');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_NOT_FOUND, $response->getStatus());
$this->assertSame('Path not available', $response->getMeta());
$this->assertFalse($success);
@rmdir('dir1');
}
public function testHandleCustomDirectoryIndex(): void
{
@mkdir('dir1');
file_put_contents('dir1/INDEX', '# Welcome to index');
$statics = $this->makeObject(['index_file' => 'INDEX']);
$request = new Request('/dir1/');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_SUCCESS, $response->getStatus());
$this->assertStringContainsString('text/plain', $response->getMeta());
$this->assertStringContainsString('# Welcome to index', $response->getBody());
$this->assertTrue($success);
@unlink('dir1/INDEX');
@rmdir('dir1');
}
public function testHandleMakeDirectoryIndex(): void
{
@mkdir('dir1');
file_put_contents('dir1/abc.txt', 'ABCDEF');
file_put_contents('dir1/def.txt', 'XFFFF');
$statics = $this->makeObject(['enable_directory_index' => true]);
$request = new Request('/dir1/');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_SUCCESS, $response->getStatus());
$this->assertSame('text/gemini', $response->getMeta());
$this->assertStringContainsString('abc.txt', $response->getBody());
$this->assertTrue($success);
@unlink('dir1/abc.txt');
@unlink('dir1/def.txt');
@rmdir('dir1');
}
public function testHandleFileNotFound(): void
{
$statics = $this->makeObject();
$request = new Request('foobar.txt');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_NOT_FOUND, $response->getStatus());
$this->assertSame('Not found!', $response->getMeta());
$this->assertTrue($success);
}
public function testCustomMimeTypes(): void
{
file_put_contents('xyz.gmi', '# Make it great');
$statics = $this->makeObject();
$request = new Request('/xyz.gmi');
[$success, $response] = $statics->handle($request);
$this->assertSame(Response::STATUS_SUCCESS, $response->getStatus());
$this->assertSame('text/gemini', $response->getMeta());
$this->assertTrue($success);
unlink('xyz.gmi');
}
public function testGetCustomMimeFromFileExtension(): void
{
$statics = $this->makeObject();
$this->assertEquals('text/gemini', $statics->getCustomMimeFromFileExtension('gmi'));
$this->assertEquals('text/gemini', $statics->getCustomMimeFromFileExtension('gemini'));
$this->assertEquals('text/gemini', $statics->getCustomMimeFromFileExtension('md'));
$this->assertEquals('text/gemini', $statics->getCustomMimeFromFileExtension('markdown'));
$this->assertEquals('text/x-ansi', $statics->getCustomMimeFromFileExtension('ans'));
$this->assertEquals('text/x-ansi', $statics->getCustomMimeFromFileExtension('ansi'));
$this->assertEquals('', $statics->getCustomMimeFromFileExtension('hoo-haw'));
}
public function testMakeDirectoryIndexWithSubdirs(): void
{
mkdir('dir1');
mkdir('dir1/sub1');
file_put_contents('dir1/foo.txt', 'foo1');
file_put_contents('dir1/sub1/bar.txt', 'bar1');
$statics = $this->makeObject();
$results = $statics->makeDirectoryIndex('', realpath('./'));
$this->assertStringContainsString('# Directory listing', $results);
$this->assertStringContainsString('=> / ..', $results);
$this->assertStringContainsString('=> dir1/ dir1/', $results);
unlink('dir1/foo.txt');
unlink('dir1/sub1/bar.txt');
rmdir('dir1/sub1');
rmdir('dir1');
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace Orbit\Tests;
use PHPUnit\Framework\TestCase;
use Monolog\Logger;
use Orbit\Config;
use Orbit\Module;
use Orbit\Request;
use Orbit\Response;
final class ModuleTest extends TestCase
{
public function testConstruct(): void
{
$module = new Module(new Config(), new Logger('foobar'));
$this->assertInstanceOf(Module::class, $module);
}
public function testHandle(): void
{
$module = new Module(new Config(), new Logger('foobar'));
[$success, $response] = $module->handle(new Request('foobar'));
$this->assertTrue($success);
$this->assertInstanceOf(Response::class, $response);
}
}