462 lines
13 KiB
PHP
Executable File
462 lines
13 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* # ergol-http
|
|
*
|
|
* Gemini capsule server through http.
|
|
*
|
|
* https://codeberg.org/adele.work/ergol-http
|
|
*
|
|
* Version 0.4.1
|
|
*
|
|
* ## Copyright 2021 Adële
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included
|
|
* in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
ini_set('display_errors', 1);
|
|
ini_set('display_startup_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
require(__DIR__.'/config.php');
|
|
|
|
// Loading config...
|
|
$conf_filename = @realpath(CONFIG_PATH);
|
|
|
|
$conf_content = file_get_contents($conf_filename);
|
|
if($conf_content===false)
|
|
die("Unable to open file ".$conf_filename."\n");
|
|
|
|
if(CONFIG_TYPE === "ergol") {
|
|
$conf_content = preg_replace('/[\x00-\x1F\x80-\xFF]/', '',$conf_content);
|
|
|
|
$conf = json_decode($conf_content);
|
|
if($conf===null)
|
|
die("Unable to parse ".$conf_filename." : ".json_last_error_msg()."\n");
|
|
}
|
|
else if(CONFIG_TYPE === "gemserv") {
|
|
//config to array of line
|
|
$gsConfigArray = explode("\n", $conf_content);
|
|
|
|
//replace all [[server]] by [server_X] (toml -> ini)
|
|
$nb = 0;
|
|
foreach($gsConfigArray as &$line) {
|
|
if($line === "[[server]]") {
|
|
$line = "[server_".$nb."]";
|
|
++$nb;
|
|
}
|
|
// comment lines with ;
|
|
if(substr($line,0,1)=='#')
|
|
$line = "; ".$line;
|
|
|
|
}
|
|
|
|
//reassemble file and parse as array
|
|
$gsConfig = implode("\n", $gsConfigArray);
|
|
$ini = parse_ini_string($gsConfig, true);
|
|
|
|
//create temp array to contain config and put general config values in it
|
|
$configTemp = array();
|
|
if(array_key_exists('port', $ini)) {
|
|
$configTemp["port"] = (int) $ini["port"];
|
|
}
|
|
else {
|
|
die('Unable to process '.CONFIG_PATH.': Missing value "port"');
|
|
}
|
|
|
|
//process for each capsule, from server_0 to server_$nb-1, put it in an array
|
|
$capsules = array();
|
|
for($i = 0; $i < $nb; ++$i) {
|
|
$key = "server_".$i;
|
|
|
|
//check that all required fields are present
|
|
if(array_key_exists('dir', $ini[$key]) && array_key_exists('hostname', $ini[$key]) && array_key_exists('hostname', $ini[$key])) {
|
|
//check absolute path
|
|
if($ini[$key]["dir"][0] == "/") {
|
|
$folder = $ini[$key]["dir"];
|
|
}
|
|
else {
|
|
$folder = "{here}/".$ini[$key]["dir"];
|
|
}
|
|
$capsules[$ini[$key]["hostname"]] = (object) array(
|
|
"folder" => $folder,
|
|
"lang" => $ini[$key]["lang"],
|
|
"lang_regex" => "/\.([a-z][a-z])\./",
|
|
"auto_index_ext" => array(".gmi"),
|
|
"redirect" => false
|
|
);
|
|
}
|
|
}
|
|
//convert capsules array to stdObj -> as in json_decode, and put it in config
|
|
$configTemp["capsules"] = (object) $capsules;
|
|
|
|
//convert array to stdObj -> as in json_decode
|
|
$conf = (object) $configTemp;
|
|
}
|
|
else {
|
|
die("Unknown config type: ".CONFIG_TYPE."\n");
|
|
}
|
|
|
|
foreach($conf->capsules as $hostname => $capsule)
|
|
{
|
|
if(empty($conf->capsules->$hostname->redirect))
|
|
{
|
|
$conf->capsules->$hostname->folder = str_replace("{here}",dirname($conf_filename),$capsule->folder);
|
|
}
|
|
else
|
|
{
|
|
unset($conf->capsules->$hostname->folder);
|
|
}
|
|
}
|
|
|
|
if(strpos($_SERVER['HTTP_HOST'],':')!==false)
|
|
$capsule = strtolower(substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'],':')));
|
|
else
|
|
$capsule = strtolower($_SERVER['HTTP_HOST']);
|
|
|
|
$response = false;
|
|
$response_code = 0;
|
|
$body = false;
|
|
|
|
if(isset($_GET['qx']))
|
|
{
|
|
$response = "OK";
|
|
$body = "# You are following a Gemini link to another server
|
|
|
|
You can't access all the Geminispace with this proxy. If you want to follow this link, you have to install ans use a Gemini client.
|
|
|
|
You asked to follow :
|
|
```gemini-url
|
|
".urldecode($_GET['qx'])."
|
|
```";
|
|
$mime="text/html";
|
|
$body=gmi2html($capsule, $body, 'en', $_GET['qx'], '');
|
|
}
|
|
|
|
if(isset($_GET['q']))
|
|
$q = $_GET['q'];
|
|
else
|
|
$q = $_SERVER['REQUEST_URI'];
|
|
|
|
if($response === false && !isset($conf->capsules->$capsule))
|
|
{
|
|
$response = "HTTP/1.1 400 BAD REQUEST";
|
|
$response_code = 0;
|
|
}
|
|
|
|
if($response === false && strpos(str_replace("\\",'/',rawurldecode($q)),'/..')!==false)
|
|
{
|
|
$response = "HTTP/1.1 400 BAD REQUEST";
|
|
$response_code = 0;
|
|
}
|
|
|
|
if(!empty($conf->capsules->$capsule->redirect))
|
|
{
|
|
// redirect to another capsule
|
|
$response = "Location: ".str_replace('gemini://','http://',$conf->capsules->$capsule->redirect.$q);
|
|
$response_code = 302;
|
|
}
|
|
elseif($response === false)
|
|
{
|
|
// search requested file
|
|
$filename = $conf->capsules->$capsule->folder.rawurldecode($q);
|
|
$lang = $conf->capsules->$capsule->lang;
|
|
if(!empty($conf->capsules->$capsule->lang_regex))
|
|
{
|
|
// search lang code in requested path (ex: file.fr.gmi)
|
|
preg_match($conf->capsules->$capsule->lang_regex, rawurldecode($q), $matches);
|
|
if(isset($matches[1]))
|
|
$lang = strtolower($matches[1]);
|
|
}
|
|
// search favicon
|
|
$favicon = @file_get_contents($conf->capsules->$capsule->folder.'/favicon.txt');
|
|
$favicon = mb_substr(trim($favicon),0,1);
|
|
}
|
|
|
|
if($response === false && $q==='/favicon.ico' && !empty($favicon))
|
|
{
|
|
// generate favicon
|
|
$image = new Imagick();
|
|
$draw = new ImagickDraw();
|
|
$pixel = new ImagickPixel( 'white' );
|
|
$image->newImage(128, 128, $pixel);
|
|
$draw->setFont('TwitterColorEmoji-SVGinOT.ttf');
|
|
$draw->setFontSize( 120 );
|
|
$draw->setFillColor('#999');
|
|
$image->annotateImage($draw, 3, 107, 0, $favicon);
|
|
$image->annotateImage($draw, 4, 106, 0, $favicon);
|
|
$image->annotateImage($draw, 5, 107, 0, $favicon);
|
|
$image->annotateImage($draw, 2, 108, 0, $favicon);
|
|
$draw->setFillColor('#666');
|
|
$image->annotateImage($draw, 6, 108, 0, $favicon);
|
|
$image->annotateImage($draw, 3, 109, 0, $favicon);
|
|
$image->annotateImage($draw, 4, 110, 0, $favicon);
|
|
$image->annotateImage($draw, 5, 109, 0, $favicon);
|
|
$draw->setFillColor('#333');
|
|
$image->annotateImage($draw, 4, 108, 0, $favicon);
|
|
$image->setImageFormat('png');
|
|
header('Content-type: image/png');
|
|
echo $image;
|
|
exit;
|
|
}
|
|
|
|
if($response === false && file_exists($filename))
|
|
{
|
|
if($response === false && is_file($filename))
|
|
{
|
|
|
|
$mime = mime_content_type($filename);
|
|
if($mime == "text/plain")
|
|
{
|
|
if(substr($q,-4)=='.gmi')
|
|
$mime = "text/gemini";
|
|
elseif(substr($q,-3)=='.md')
|
|
$mime = "text/markdown";
|
|
elseif(substr($q,-4)=='.html')
|
|
$mime = "text/html";
|
|
}
|
|
$response = "OK";
|
|
$body = file_get_contents($filename);
|
|
if($mime=="text/gemini")
|
|
{
|
|
$mime="text/html";
|
|
$body=gmi2html($capsule, $body, $lang,
|
|
'gemini://'.$capsule.($conf->port==1965?'':(':'.$conf->port)).$q,
|
|
$favicon);
|
|
}
|
|
}
|
|
|
|
if($response === false && is_dir($filename))
|
|
{
|
|
// if path is a directory name redirect into it
|
|
if(substr($filename,-1)!='/')
|
|
{
|
|
$response = "Location: ".$q."/";
|
|
$response_code = 302;
|
|
}
|
|
else
|
|
{
|
|
$mime = "text/html";
|
|
if(file_exists($filename.'/index.gmi'))
|
|
{
|
|
// open default file index.gmi
|
|
$response = "OK";
|
|
$filename = $filename.'/index.gmi';
|
|
$body = file_get_contents($filename);
|
|
$body = gmi2html($capsule, $body, $lang,
|
|
'gemini://'.$capsule.($conf->port==1965?'':(':'.$conf->port)).$q,
|
|
$favicon);
|
|
}
|
|
elseif(is_array($conf->capsules->$capsule->auto_index_ext))
|
|
{
|
|
// build auto index
|
|
$response = "OK";
|
|
$body = "# ".$capsule." ".basename($filename)."\r\n";
|
|
$body .= "=> ../ [..]\r\n";
|
|
// three blocks
|
|
$items_dir=array(); // sub directories
|
|
$items_gmi=array(); // gmi file chronogically desc
|
|
$items_oth=array(); // other files
|
|
$d = dir($filename);
|
|
while (false !== ($entry = $d->read()))
|
|
{
|
|
if(substr($entry,0,1)=='.')
|
|
{
|
|
// dir itself
|
|
continue;
|
|
}
|
|
if(is_dir($filename.'/'.$entry) &&
|
|
!in_array('/', $conf->capsules->$capsule->auto_index_ext))
|
|
{
|
|
// folder ext "/" not in auto_index conf
|
|
continue;
|
|
}
|
|
if(is_file($filename.'/'.$entry) &&
|
|
!in_array(substr($entry,strrpos($entry,'.')), $conf->capsules->$capsule->auto_index_ext))
|
|
{
|
|
// ext not in auto_index conf
|
|
continue;
|
|
} $link_name = $entry;
|
|
if(substr($entry,-4)=='.gmi')
|
|
{
|
|
// build feed for subscriptions for .gmi files,
|
|
// adding date YYYY-MM-DD in link if not file name
|
|
// see specs gemini://gemini.circumlunar.space/docs/companion/subscription.gmi
|
|
$entry_name = str_replace('_',' ',substr($entry,0,-4));
|
|
if(!preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])\s$/",substr($entry_name,0,11)))
|
|
$link_name = date("Y-m-d", filemtime($filename.'/'.$entry))." ".$entry_name;
|
|
else
|
|
$link_name = " ".$entry_name;
|
|
$items_gmi[$link_name." ".$entry] = "=> ".rawurlencode($entry)." ".$link_name;
|
|
}
|
|
elseif(is_dir($filename.'/'.$entry))
|
|
{
|
|
// sub directory
|
|
$link_name = "[".$entry."]";
|
|
$items_dir[$entry] = "=> ".rawurlencode($entry)."/ ".$link_name;
|
|
}
|
|
else
|
|
{
|
|
// other file ext
|
|
$items_oth[$entry] = "=> ".rawurlencode($entry)." ".$link_name;
|
|
}
|
|
}
|
|
$d->close();
|
|
ksort($items_dir);
|
|
krsort($items_gmi);
|
|
ksort($items_oth);
|
|
if(count($items_dir)>0)
|
|
$body .= implode("\r\n", $items_dir)."\r\n";
|
|
if(count($items_gmi)>0)
|
|
$body .= implode("\r\n", $items_gmi)."\r\n";
|
|
if(count($items_oth)>0)
|
|
$body .= implode("\r\n", $items_oth)."\r\n";
|
|
$body = gmi2html($capsule, $body, $lang,
|
|
'gemini://'.$capsule.($conf->port==1965?'':(':'.$conf->port)).$q,
|
|
$favicon);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if($response === false)
|
|
{
|
|
$response = "HTTP/1.1 404 NOT FOUND";
|
|
$response_code = 0;
|
|
}
|
|
|
|
if($response != "OK")
|
|
{
|
|
header($response, true, $response_code);
|
|
exit;
|
|
}
|
|
|
|
header("Content-Type: ".$mime, true);
|
|
header("Content-Length: ".strlen($body), true);
|
|
echo $body;
|
|
exit;
|
|
|
|
|
|
function gmi2html($capsule, $body, $lang, $urlgem, $favicon)
|
|
{
|
|
if(isset($_SERVER['REQUEST_SCHEME'])) {
|
|
$scheme = $_SERVER['REQUEST_SCHEME'];
|
|
}
|
|
else if(isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS'])) {
|
|
$scheme = 'https';
|
|
}
|
|
else {
|
|
$scheme = 'http';
|
|
}
|
|
$title='';
|
|
$lines=array();
|
|
$tocs=array();
|
|
$lev1=0;
|
|
$lev2=0;
|
|
$lev3=0;
|
|
$pre=false;
|
|
$glines = explode("\n", $body);
|
|
foreach($glines as $line)
|
|
{
|
|
if($pre && substr(trim($line, "\r\n"),0,3)!='```')
|
|
{
|
|
$lines[] = str_replace(array('&','<','>','"',"'"), array('&','<','>','"','''), $line);
|
|
continue;
|
|
}
|
|
$line=trim($line, "\r\n");
|
|
$prefix = explode(' ',substr($line,0,3),2);
|
|
$prefix=$prefix[0];
|
|
// if no space before titles
|
|
if(substr($line,0,1)=='#')
|
|
$prefix='#';
|
|
if(substr($line,0,2)=='##')
|
|
$prefix='##';
|
|
if(substr($line,0,3)=='###')
|
|
$prefix='###';
|
|
if($prefix=="```")
|
|
{
|
|
if($pre)
|
|
$lines[]='</pre>';
|
|
else
|
|
$lines[]='<pre title="'.htmlentities(substr($line,3)).'">';
|
|
$pre=!$pre;
|
|
continue;
|
|
}
|
|
if($prefix=="#" && empty($title))
|
|
$title = trim(substr($line,2));
|
|
switch($prefix)
|
|
{
|
|
case "#":
|
|
$lev1++;
|
|
$lev2=0;
|
|
$lev3=0;
|
|
$levid = $lev1;
|
|
$lines[] = '<h1 id="'.$levid.'">'.trim(htmlentities(substr($line,1))).'</h1>';
|
|
$tocs[] = '<li class="l1"><a href="#'.$levid.'">'.trim(htmlentities(substr($line,1))).'</a></li>';
|
|
break;
|
|
case "##":
|
|
$lev2++;
|
|
$lev3=0;
|
|
$levid = $lev1.'-'.$lev2;
|
|
$lines[] = '<h2 id="'.$levid.'">'.trim(htmlentities(substr($line,2))).'</h2>';
|
|
$tocs[] = '<li class="l2"><a href="#'.$levid.'">'.trim(htmlentities(substr($line,2))).'</a></li>';
|
|
break;
|
|
case "###":
|
|
$lev3++;
|
|
$levid = $lev1.'-'.$lev2.'-'.$lev3;
|
|
$lines[] = '<h3 id="'.$levid.'">'.trim(htmlentities(substr($line,3))).'</h3>';
|
|
$tocs[] = '<li class="l3"><a href="#'.$levid.'">'.trim(htmlentities(substr($line,3))).'</a></li>';
|
|
break;
|
|
case ">":
|
|
$lines[] = "<blockquote>".htmlentities(substr($line,2))."</blockquote>";
|
|
break;
|
|
case "*":
|
|
$lines[] = "<li>".htmlentities(substr($line,2))."</li>";
|
|
break;
|
|
case "=>":
|
|
$lines[]='<p>';
|
|
$link = explode(' ', substr($line,3), 2);
|
|
if(str_starts_with($link[0], 'gemini://'.$capsule)) {
|
|
$lines[] = '<a href="'.str_replace('gemini://'.$capsule,$scheme.'://'.$_SERVER['HTTP_HOST'], $link[0]).'">'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1])."</a>";
|
|
}
|
|
else if(str_starts_with($link[0], 'gemini://')) {
|
|
$lines[] = '<a href="/?qx='.urlencode($link[0]).'">'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1])."</a>";
|
|
}
|
|
else {
|
|
$lines[] = '<a href="'.$link[0].'">'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1])."</a>";
|
|
}
|
|
if(strpos($link[0], '://')===false && // relative image
|
|
in_array(strtolower(substr($link[0],-4)),array('.jpg','.png','.gif','jpeg','webp')) )
|
|
$lines[] = ' 🖼️ <div class="inline-img"><img src="'.$link[0].'" alt="'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1]).'" /></div>';
|
|
$lines[]='</p>';
|
|
break;
|
|
default:
|
|
$lines[] = "<p>".htmlentities($line)."</p>";
|
|
break;
|
|
}
|
|
}
|
|
$style = file_get_contents(__DIR__.'/style.css');
|
|
ob_start();
|
|
include "template.php";
|
|
$html = ob_get_contents();
|
|
ob_end_clean();
|
|
return $html;
|
|
}
|