diff --git a/README.md b/README.md new file mode 100644 index 0000000..785af2a --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Gemini-PHP +Gemini-PHP is a Gemini server written in PHP by @neil@glasgow.social. +It's designed more for teaching than practical use. That's said - it's very simple to get up and running and we're hosting this page on it - it seems to be performing well. +If you have any questions or want to get in touch, you can join our community on Matrix at #gemini-php:glasgow.social + +## How to install +* Download via git +``` +git clone https://coding.openguide.co.uk/git/gemini-php/ +``` + +* Enter the project directory and create a certificate for your server (a self signed certificate is fine, in fact it's encouraged!) +``` +cd gemini-php +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 +``` +* Combine your private key with the certificate and put in the certs directory + +``` +cp cert.pem certs/combined.pem +cat key.pem >> certs/combined.pem +``` +* Create a config file from the sample +``` +cp config.php.sample config.php +``` +* Then edit it with the location of your new certificate - most other options are optional +* Start your server with +``` +php server.php +``` +* You should be able to visit your new server in any Gemini client (remember to open your firewall if needed - post 1965) + +## Using Gemini-PHP +* The basic index file is located in hosts/default/index.gemini - edit this to get started +* Gemini-PHP supports multiple virtual hosts, just create a directory with the name of the domain you expect to receive requests for, i.e. +``` +mkdir hosts/glasgow.social +mkdir hosts/projects.glasgow.social +``` + +## Running as a service +To set up the server as a service, create the following file in /etc/systemd/system/gemini-php.service +``` +[Unit] +Description=Gemini-PHP Service + +[Service] +User=gemini +Type=simple +TimeoutSec=0 +WorkingDirectory=/home/gemini/gemini-php/ +PIDFile=/var/run/gemini-php.pid +ExecStart=/usr/bin/php -f /home/gemini/gemini-php/server.php +KillMode=process + +Restart=on-failure +RestartSec=42s + +[Install] +WantedBy=default.target +``` +Note, customise the above to the user you are running gemini-php as (we recommend creating a new user account for this to keep it relatively isolated) as well as the path to the script. +Enable the script with systemctl +``` +sudo systemctl enable gemini-php +sudo systemctl start gemini-php +systemctl status gemini-php + +sudo systemctl stop gemini-php +``` diff --git a/config.php.sample b/config.php.sample index 3faf586..7b5a38e 100644 --- a/config.php.sample +++ b/config.php.sample @@ -5,7 +5,7 @@ * * This is your certificate file. A self-signed certificate is acceptable here. * You can generate one using: - * + * * openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 * * Then combine the key and certificate and copy them to the certs directory: @@ -34,10 +34,8 @@ $config['certificate_passphrase'] = ''; // Default index file. If a path isn't specified then the server will // default to an index file (like index.html on a web server). -//$config['default_index_file'] = "index.gemini"; +//$config['default_index_file'] = "index.gmi"; // Logging, setting this to false will disable logging (default is on/true); //$config['logging'] = true; //$config['log_file'] = "logs/gemini-php.log"; - -?> diff --git a/gemini.class.php b/gemini.class.php index a27823b..310fff2 100644 --- a/gemini.class.php +++ b/gemini.class.php @@ -3,67 +3,92 @@ error_reporting(E_ALL); set_time_limit(0); ob_implicit_flush(); -class Gemini { +// Based on: +// gemini://glasgow.social/gemini-php +class Gemini { function __construct($config) { - if(empty($config['certificate_file'])) - die("Missing certificate file. Edit config.php\n"); - $this->ip = "0"; - $this->port = "1965"; - $this->data_dir = "hosts/"; - $this->default_host_dir = "default/"; - $this->default_index_file = "index.gemini"; + if (empty($config['certificate_file'])) { + die('Missing certificate file. Edit config.php\n'); + } + + $this->ip = '0'; + $this->port = '1965'; + $this->data_dir = 'hosts/'; + $this->default_host_dir = 'default/'; + $this->default_index_file = 'index.gmi'; $this->logging = 1; $this->log_file = "logs/gemini-php.log"; $this->log_sep = "\t"; - $settings = array('ip', 'port', 'data_dir', 'default_host_dir', 'default_index_file', - 'certificate_file', 'certificate_passphrase'); - foreach($settings as $setting_key) { - if(!empty($config[$setting_key])) + + $settings = array( + 'ip', 'port', + 'data_dir', + 'default_host_dir', + 'default_index_file', + 'certificate_file', + 'certificate_passphrase' + ); + + foreach ($settings as $setting_key) { + if (!empty($config[$setting_key])) { $this->$setting_key = $config[$setting_key]; + } } - // append the required filepath slashes if they're missing - if(substr($this->data_dir, -1) != "/") + + // Append the required filepath slashes if they're missing + if (substr($this->data_dir, -1) != "/") { $this->data_dir .= "/"; - if(substr($this->default_host_dir, -1) != "/") + } + if (substr($this->default_host_dir, -1) != "/") { $this->default_host_dir .= "/"; - if($this->logging) { - if(!file_exists($this->log_file)) { + } + if ($this->logging) { + if (!file_exists($this->log_file)) { $this->log_to_file("Log created", null, null, null, null); } - if(!is_writable($this->log_file)) { + if (!is_writable($this->log_file)) { die("{$this->log_file} is not writable.\n"); } } - if(!is_readable($this->certificate_file)) + if (!is_readable($this->certificate_file)) { die("Certificate file {$this->certificate_file} not readable.\n"); + } } function parse_request($request) { - $url = trim($request); // strip from the end + $url = trim($request); // Strip from the end return parse_url($url); } function get_valid_hosts() { - $dirs = array_map('basename', glob($this->data_dir.'*', GLOB_ONLYDIR)); + $dirs = array_map('basename', glob($this->data_dir . '*', GLOB_ONLYDIR)); return $dirs; } function get_status_code($filepath) { - if(is_file($filepath) and file_exists($filepath)) - return "20"; - if(!file_exists($filepath)) - return "51"; - return "50"; + if (is_file($filepath) and file_exists($filepath)) { + return '20'; + } + if (!file_exists($filepath)) { + //echo("File $filepath doesn't exist\n"); + return '51'; + } + return '50'; } function get_mime_type($filepath) { $type = mime_content_type($filepath); - // we need a way to detect gemini file types, which PHP doesn't + // We need a way to detect gemini file types, which PHP doesn't // so.. if it ends with gemini (or if it has no extension), assume $path_parts = pathinfo($filepath); - if(empty($path_parts['extension']) or $path_parts['extension'] == "gemini") - $type = "text/gemini"; + + if (empty($path_parts['extension']) + or $path_parts['extension'] === 'gemini' + or $path_parts['extension'] === 'gmi' + ) { + $type = 'text/gemini'; + } return $type; } @@ -79,31 +104,42 @@ class Gemini { * @return string */ function get_filepath($url) { - $hostname = ""; - if(!is_array($url)) + $hostname = ''; + + if (!is_array($url)) { return false; - if(!empty($url['host'])) + } + if (!empty($url['host'])) { $hostname = $url['host']; + } $valid_hosts = $this->get_valid_hosts(); - if(!in_array($hostname, $valid_hosts)) - $hostname = "default"; + + if (!in_array($hostname, $valid_hosts)) { + $hostname = 'default'; + } // Kristall Browser is adding "__" to the end of the filenames // wtf am I missing? // also removing ".." to mitigate against directory traversal - $url['path'] = str_replace(array("..", "__"), "", $url['path']); - // force an index file to be appended if a filename is missing - if(empty($url['path'])) { - $url['path'] = "/".$this->default_index_file; - } elseif(substr($url['path'], -1) == "/") { + $url['path'] = str_replace(array('..', '__'), '', $url['path']); + + // Force an index file to be appended if a filename is missing + if (empty($url['path'])) { + $url['path'] = '/' . $this->default_index_file; + } elseif(substr($url['path'], -1) === '/') { $url['path'] .= $this->default_index_file; } - $valid_data_dir = dirname(__FILE__)."/".$this->data_dir; + $valid_data_dir = dirname(__FILE__) . '/' . $this->data_dir; $return_path = $this->data_dir.$hostname.$url['path']; - // check the real path is in the data_dir (path traversal sanity check) - if(substr(realpath($return_path),0, strlen($valid_data_dir)) == $valid_data_dir) { + + if (is_link($return_path)) { + return $return_path; + } + + // Check the real path is in the data_dir (path traversal sanity check) + if (substr(realpath($return_path), 0, strlen($valid_data_dir)) === $valid_data_dir) { return $return_path; } return false; @@ -112,10 +148,11 @@ class Gemini { function log_to_file($ip, $status_code, $meta, $filepath, $filesize) { $ts = date("Y-m-d H:i:s", strtotime('now')); $this->log_sep; - $str = $ts.$this->log_sep.$ip.$this->log_sep.$status_code.$this->log_sep. - $meta.$this->log_sep.$filepath.$this->log_sep.$filesize."\n"; + + $str = $ts.$this->log_sep . $ip . $this->log_sep + . $status_code . $this->log_sep + . $meta.$this->log_sep . $filepath . $this->log_sep + . $filesize . "\n"; file_put_contents($this->log_file, $str, FILE_APPEND); } } - -?> diff --git a/hosts/default/index.gemini b/hosts/default/index.gemini deleted file mode 100644 index 95b1f9a..0000000 --- a/hosts/default/index.gemini +++ /dev/null @@ -1,15 +0,0 @@ -# Success! -Your Gemini server is up and running! - -You can read more about this server at -=> gemini://glasgow.social/gemini-php - -Join the community on Matrix at #gemini-php:glasgow.social - -## Getting Started -Let's take 'gemini.com' as an example. -* Generate a certificate and place it in the 'certs' directory named gemini.com.pem -* Create a directory to server files from in the 'hosts' directory named gemini.com (e.g. mkdir hosts/gemini.com) -* A file called index.gemini will be served if you haven't specified a path (this file is located at hosts/default/index.gemini) - -=> natalie.jpg diff --git a/server.php b/server.php index d1be889..920cf58 100644 --- a/server.php +++ b/server.php @@ -4,8 +4,9 @@ * Version 0.1, Oct 2020 */ -if(!require("config.php")) +if(!require("config.php")) { die("config.php is missing. Copy config.php.sample to config.php and customise your settings"); +} require("gemini.class.php"); $g = new Gemini($config); @@ -25,6 +26,10 @@ $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_SERVER & ~ STREAM_CRYPTO_METHOD_TLSv1_0_SERVER & ~ STREAM_CRYPTO_METHOD_TLSv1_1_SERVER; +$cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_3_SERVER; + +print("Running server on port $g->port\n"); + while(true) { $forkedSocket = stream_socket_accept($socket, "-1", $remoteIP); @@ -42,17 +47,18 @@ while(true) { $meta = ""; $filesize = 0; - if($status_code == "20") { + if ($status_code == "20") { $meta = $g->get_mime_type($filepath); - $content = file_get_contents($filepath); + $content = file_get_contents($filepath); $filesize = filesize($filepath); } else { $meta = "Not found"; } - $status_line = $status_code." ".$meta; - if($g->logging) - $g->log_to_file($remoteIP,$status_code, $meta, $filepath, $filesize); + $status_line = $status_code . " " . $meta; + if ($g->logging) { + $g->log_to_file($remoteIP, $status_code, $meta, $filepath, $filesize); + } $status_line .= "\r\n"; fwrite($forkedSocket, $status_line); @@ -62,5 +68,3 @@ while(true) { fclose($forkedSocket); } - -?>