mirror of
https://github.com/YGGverse/next.git
synced 2025-01-25 22:34:21 +00:00
implement multi-protocol async server based on ratchet library #1
This commit is contained in:
parent
296525aac3
commit
ec60ef5e5b
73
README.md
73
README.md
@ -1,60 +1,71 @@
|
||||
# next
|
||||
|
||||
PHP 8 Server for [NEX Protocol](https://nightfall.city/nex/info/specification.txt), based on the [nex-php](https://github.com/YGGverse/nex-php) library
|
||||
PHP 8 server for different protocols, based on [Ratchet](https://github.com/ratchetphp/Ratchet) asynchronous socket library.
|
||||
|
||||
## Features
|
||||
|
||||
* Asynchronous connections
|
||||
* Multi-host
|
||||
* Multiple protocols:
|
||||
* [x] [NEX](https://nightfall.city/nex/info/specification.txt)
|
||||
* [ ] [Gemini](https://geminiprotocol.net)
|
||||
* Detailed event log
|
||||
* Optional:
|
||||
* directory listing navigation with safe filesystem access
|
||||
* custom index file names
|
||||
* custom failure page
|
||||
* Flexible server configuration by environment arguments
|
||||
|
||||
## Install
|
||||
|
||||
* `git clone https://github.com/YGGverse/next.git`
|
||||
* `cd next` - navigate into the server directory
|
||||
* `cd next` - navigate into the project directory
|
||||
* `composer update` - grab latest dependencies
|
||||
|
||||
## NEX
|
||||
|
||||
Optimal to serve static files
|
||||
|
||||
For security reasons, `next` server prevents any access to the hidden files (started with dot)
|
||||
## Launch
|
||||
|
||||
### Start
|
||||
|
||||
Create as many servers as wanted by providing different `host` and `port` using optional arguments
|
||||
Create as many servers as wanted by providing different `type`, `host`, `port` and other arguments.
|
||||
|
||||
* for security reasons, `next` server prevents any access to the hidden files (started with dot).\
|
||||
* also, clients can't access any data out the `root` path, defined on server startup
|
||||
|
||||
Simple example:
|
||||
|
||||
``` bash
|
||||
php src/nex.php host=127.0.0.1 port=1900 path=/target/dir
|
||||
php src/server.php type=nex host=127.0.0.1 port=1900 root=/target/dir
|
||||
```
|
||||
|
||||
* `host` and `port` is optional, read [arguments documentation](#arguments) for details!
|
||||
|
||||
#### Arguments
|
||||
|
||||
##### Required
|
||||
|
||||
* `path` - **absolute path** to the public directory
|
||||
* `type` - server protocol, supported options:
|
||||
* `nex` - [NEX Protocol](https://nightfall.city/nex/info/specification.txt)
|
||||
* `root` - **absolute path** to the public directory
|
||||
|
||||
##### Optional
|
||||
|
||||
* `host` - `127.0.0.1` by default
|
||||
* `port` - `1900` by default
|
||||
* `file` - index **filename** that server try to open on directory path requested, disabled by default
|
||||
* `list` - show content listing in the requested directory (when index file not found), `yes` by default
|
||||
* `fail` - **filepath** that contain failure text or template (e.g. `error.gmi`), `fail` text by default
|
||||
* `size` - limit request length in bytes, `1024` by default
|
||||
* `dump` - query log, blank to disable, default: `[{time}] [{code}] {host}:{port} {path} {real} {size} bytes`
|
||||
* `{time}` - event time in `c` format
|
||||
* `{code}` - formal response code: `1` - found, `0` - not found
|
||||
* `{host}` - peer host
|
||||
* `{port}` - peer port
|
||||
* `{path}` - path requested
|
||||
* `{real}` - **realpath** returned
|
||||
* `{size}` - response size in bytes
|
||||
* `port` - depends of server `type` by default
|
||||
* `file` - index **file name** that server try to open on directory path requested, disabled by default
|
||||
* `list` - show content listing in the requested directory (when index file not found), enabled by default
|
||||
* `time` - show file modification time as the alt text in directory listing, disabled by default
|
||||
* `fail` - **absolute path** to the failure template file (e.g. `/path/to/error.gmi`), disabled by default
|
||||
* `dump` - query log, enabled by default
|
||||
|
||||
### Autostart
|
||||
|
||||
Launch server as the `systemd` service
|
||||
#### systemd
|
||||
|
||||
Following example mean you have `next` server installed into home directory of `next` user (`useradd -m next`)
|
||||
|
||||
1. `mkdir /home/next/public` - make sure you have created public folder
|
||||
2. `sudo nano /etc/systemd/system/next.service` - create new service:
|
||||
|
||||
``` next.service
|
||||
# /etc/systemd/system/next.service
|
||||
|
||||
[Unit]
|
||||
After=network.target
|
||||
|
||||
@ -62,7 +73,7 @@ After=network.target
|
||||
Type=simple
|
||||
User=next
|
||||
Group=next
|
||||
ExecStart=/usr/bin/php /home/next/next/src/nex.php path=/home/next/public
|
||||
ExecStart=/usr/bin/php /home/next/next/src/server.php type=nex root=/home/next/public
|
||||
StandardOutput=file:/home/next/debug.log
|
||||
StandardError=file:/home/next/error.log
|
||||
Restart=on-failure
|
||||
@ -71,6 +82,6 @@ Restart=on-failure
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. `sudo systemctl daemon-reload` - reload systemd configuration
|
||||
4. `sudo systemctl enable next` - enable `next` service on system startup
|
||||
5. `sudo systemctl start next` - start `next` server
|
||||
* `systemctl daemon-reload` - reload systemd configuration
|
||||
* `systemctl enable next` - enable service on system startup
|
||||
* `systemctl start next` - start server
|
||||
|
@ -5,7 +5,7 @@
|
||||
"homepage": "https://github.com/yggverse/pulsar",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"yggverse/nex": "^1.1"
|
||||
"cboden/ratchet": "^0.4.4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
|
5
default.json
Normal file
5
default.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"host":"127.0.0.1",
|
||||
"list":true,
|
||||
"dump":true
|
||||
}
|
263
src/Controller/Nex.php
Normal file
263
src/Controller/Nex.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace Yggverse\Next\Controller;
|
||||
|
||||
use \Ratchet\MessageComponentInterface;
|
||||
|
||||
class Nex implements MessageComponentInterface
|
||||
{
|
||||
private \Yggverse\Next\Model\Environment $_environment;
|
||||
private \Yggverse\Next\Model\Filesystem $_filesystem;
|
||||
|
||||
public function __construct(
|
||||
\Yggverse\Next\Model\Environment $environment,
|
||||
\Yggverse\Next\Model\Filesystem $filesystem
|
||||
) {
|
||||
// Init environment
|
||||
$this->_environment = $environment;
|
||||
|
||||
// Init filesystem
|
||||
$this->_filesystem = $filesystem;
|
||||
|
||||
// Check port is defined
|
||||
if (!$this->_environment->get('port'))
|
||||
{
|
||||
// Set protocol defaults
|
||||
$this->_environment->set('port', 1900);
|
||||
}
|
||||
|
||||
// Dump event
|
||||
if ($this->_environment->get('dump'))
|
||||
{
|
||||
print(
|
||||
str_replace(
|
||||
[
|
||||
'{time}',
|
||||
'{host}',
|
||||
'{port}',
|
||||
'{root}'
|
||||
],
|
||||
[
|
||||
(string) date('c'),
|
||||
(string) $this->_environment->get('host'),
|
||||
(string) $this->_environment->get('port'),
|
||||
(string) $this->_filesystem->root()
|
||||
],
|
||||
_('[{time}] [init] server started at {host}:{port}{root}')
|
||||
) . PHP_EOL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function onOpen(
|
||||
\Ratchet\ConnectionInterface $connection
|
||||
) {
|
||||
// Dump event
|
||||
if ($this->_environment->get('dump'))
|
||||
{
|
||||
print(
|
||||
str_replace(
|
||||
[
|
||||
'{time}',
|
||||
'{host}',
|
||||
'{crid}'
|
||||
],
|
||||
[
|
||||
(string) date('c'),
|
||||
(string) $connection->remoteAddress,
|
||||
(string) $connection->resourceId
|
||||
],
|
||||
_('[{time}] [open] incoming connection {host}#{crid}')
|
||||
) . PHP_EOL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function onMessage(
|
||||
\Ratchet\ConnectionInterface $connection,
|
||||
$request
|
||||
) {
|
||||
// Define response
|
||||
$response = null;
|
||||
|
||||
// Filter request
|
||||
$request = trim(
|
||||
$request
|
||||
);
|
||||
|
||||
// Build absolute realpath
|
||||
$realpath = $this->_filesystem->absolute(
|
||||
$request
|
||||
);
|
||||
|
||||
// Make sure realpath valid to continue
|
||||
if ($this->_filesystem->valid($realpath))
|
||||
{
|
||||
// Route
|
||||
switch (true)
|
||||
{
|
||||
// File request
|
||||
case $file = $this->_filesystem->file($realpath):
|
||||
|
||||
// Return file content
|
||||
$response = $file;
|
||||
|
||||
break;
|
||||
|
||||
// Directory request
|
||||
case $list = $this->_filesystem->list($realpath):
|
||||
|
||||
// Try index file on defined
|
||||
if ($index = $this->_filesystem->file($realpath . $this->_environment->get('file')))
|
||||
{
|
||||
// Return index file content
|
||||
$response = $index;
|
||||
}
|
||||
|
||||
// Listing enabled
|
||||
else if ($this->_environment->get('list'))
|
||||
{
|
||||
// FS map
|
||||
$line = [];
|
||||
|
||||
foreach ($list as $item)
|
||||
{
|
||||
// Build gemini text link
|
||||
$link = ['=>'];
|
||||
|
||||
if ($item['name'])
|
||||
{
|
||||
$link[] = $item['file'] ? $item['name']
|
||||
: $item['name'] . '/';
|
||||
}
|
||||
|
||||
if ($item['time'] && $this->_environment->get('time'))
|
||||
{
|
||||
$link[] = date('Y-m-d', $item['time']);
|
||||
}
|
||||
|
||||
// Append link to the new line
|
||||
$line[] = implode(' ', $link);
|
||||
}
|
||||
|
||||
// Merge lines to response
|
||||
$response = implode(
|
||||
PHP_EOL,
|
||||
$line
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Dump event
|
||||
if ($this->_environment->get('dump'))
|
||||
{
|
||||
// Print debug from template
|
||||
print(
|
||||
str_ireplace(
|
||||
[
|
||||
'{time}',
|
||||
'{host}',
|
||||
'{crid}',
|
||||
'{path}',
|
||||
'{real}',
|
||||
'{size}'
|
||||
],
|
||||
[
|
||||
(string) date('c'),
|
||||
(string) $connection->remoteAddress,
|
||||
(string) $connection->resourceId,
|
||||
(string) str_replace(
|
||||
'%',
|
||||
'%%',
|
||||
$request
|
||||
),
|
||||
(string) str_replace(
|
||||
'%',
|
||||
'%%',
|
||||
$realpath
|
||||
),
|
||||
(string) mb_strlen(
|
||||
$response
|
||||
)
|
||||
],
|
||||
_('[{time}] [message] incoming connection {host}#{crid} "{path}" > "{real}" {size} bytes')
|
||||
) . PHP_EOL
|
||||
);
|
||||
}
|
||||
|
||||
// Noting to return?
|
||||
if (empty($response))
|
||||
{
|
||||
// Try failure file on defined
|
||||
if ($fail = $this->_filesystem->file($this->_environment->get('fail')))
|
||||
{
|
||||
$response = $fail;
|
||||
}
|
||||
}
|
||||
|
||||
// Send response
|
||||
$connection->send(
|
||||
$response
|
||||
);
|
||||
|
||||
// Disconnect
|
||||
$connection->close();
|
||||
}
|
||||
|
||||
public function onClose(
|
||||
\Ratchet\ConnectionInterface $connection
|
||||
) {
|
||||
// Dump event
|
||||
if ($this->_environment->get('dump'))
|
||||
{
|
||||
print(
|
||||
str_replace(
|
||||
[
|
||||
'{time}',
|
||||
'{host}',
|
||||
'{crid}'
|
||||
],
|
||||
[
|
||||
(string) date('c'),
|
||||
(string) $connection->remoteAddress,
|
||||
(string) $connection->resourceId
|
||||
],
|
||||
_('[{time}] [close] incoming connection {host}#{crid}')
|
||||
) . PHP_EOL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function onError(
|
||||
\Ratchet\ConnectionInterface $connection,
|
||||
\Exception $exception
|
||||
) {
|
||||
// Dump event
|
||||
if ($this->_environment->get('dump'))
|
||||
{
|
||||
print(
|
||||
str_replace(
|
||||
[
|
||||
'{time}',
|
||||
'{host}',
|
||||
'{crid}',
|
||||
'{info}'
|
||||
],
|
||||
[
|
||||
(string) date('c'),
|
||||
(string) $connection->remoteAddress,
|
||||
(string) $connection->resourceId,
|
||||
(string) $exception->getMessage()
|
||||
],
|
||||
_('[{time}] [error] incoming connection {host}#{crid} reason: {info}')
|
||||
) . PHP_EOL
|
||||
);
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
$connection->close();
|
||||
}
|
||||
}
|
84
src/Model/Environment.php
Normal file
84
src/Model/Environment.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Yggverse\Next\Model;
|
||||
|
||||
class Environment
|
||||
{
|
||||
private array $_config;
|
||||
|
||||
public function __construct(
|
||||
array $argv,
|
||||
array $default = []
|
||||
) {
|
||||
foreach ($default as $key => $value)
|
||||
{
|
||||
$this->_config[$key] = (string) $value;
|
||||
}
|
||||
|
||||
foreach ($argv as $value)
|
||||
{
|
||||
if (preg_match('/^(?<key>[^=]+)=(?<value>.*)$/', $value, $argument))
|
||||
{
|
||||
$this->_config[mb_strtolower($argument['key'])] = (string) $argument['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get(
|
||||
string $key
|
||||
): mixed
|
||||
{
|
||||
$key = mb_strtolower(
|
||||
$key
|
||||
);
|
||||
|
||||
return isset($this->_config[$key]) ? $this->_config[$key]
|
||||
: null;
|
||||
}
|
||||
|
||||
public function set(
|
||||
string $key,
|
||||
string $value,
|
||||
bool $semantic = true
|
||||
): void
|
||||
{
|
||||
if ($semantic)
|
||||
{
|
||||
$_value = mb_strtolower(
|
||||
$value
|
||||
);
|
||||
|
||||
switch (true)
|
||||
{
|
||||
case in_array(
|
||||
$_value,
|
||||
[
|
||||
'1',
|
||||
'yes',
|
||||
'true',
|
||||
'enable'
|
||||
]
|
||||
): $value = true;
|
||||
|
||||
break;
|
||||
|
||||
case in_array(
|
||||
$_value,
|
||||
[
|
||||
'0',
|
||||
'no',
|
||||
'null',
|
||||
'false',
|
||||
'disable'
|
||||
]
|
||||
): $value = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->_config[mb_strtolower($key)] = $value;
|
||||
}
|
||||
}
|
253
src/Model/Filesystem.php
Normal file
253
src/Model/Filesystem.php
Normal file
@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Yggverse\Next\Model;
|
||||
|
||||
class Filesystem
|
||||
{
|
||||
private string $_root;
|
||||
|
||||
public function __construct(?string $path)
|
||||
{
|
||||
// Require path value to continue
|
||||
if (empty($path))
|
||||
{
|
||||
throw new \Exception(
|
||||
_('root path required!')
|
||||
);
|
||||
}
|
||||
|
||||
// Require absolute path
|
||||
if (!str_starts_with($path, DIRECTORY_SEPARATOR))
|
||||
{
|
||||
throw new \Exception(
|
||||
_('root path not absolute!')
|
||||
);
|
||||
}
|
||||
|
||||
// Exclude symlinks and relative entities, append slash
|
||||
if (!$realpath = $this->_realpath($path))
|
||||
{
|
||||
throw new \Exception(
|
||||
_('could not build root realpath!')
|
||||
);
|
||||
}
|
||||
|
||||
// Root must be directory
|
||||
if (!is_dir($realpath))
|
||||
{
|
||||
throw new \Exception(
|
||||
_('root path is not directory!')
|
||||
);
|
||||
}
|
||||
|
||||
// Check root path does not contain hidden context
|
||||
if (str_contains($realpath, DIRECTORY_SEPARATOR . '.'))
|
||||
{
|
||||
throw new \Exception(
|
||||
_('root path must not contain hidden context!')
|
||||
);
|
||||
}
|
||||
|
||||
// Done!
|
||||
$this->_root = $realpath;
|
||||
}
|
||||
|
||||
public function root(): string
|
||||
{
|
||||
return $this->_root;
|
||||
}
|
||||
|
||||
public function file(?string $realpath): ?string
|
||||
{
|
||||
if (!$this->valid($realpath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_file($realpath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return file_get_contents(
|
||||
$realpath
|
||||
);
|
||||
}
|
||||
|
||||
public function list(
|
||||
?string $realpath,
|
||||
string $sort = 'name',
|
||||
int $order = SORT_ASC,
|
||||
int $method = SORT_STRING | SORT_NATURAL | SORT_FLAG_CASE
|
||||
): ?array
|
||||
{
|
||||
// Validate requested path
|
||||
if (!$this->valid($realpath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make sure requested path is directory
|
||||
if (!is_dir($realpath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Begin list builder
|
||||
$directories = [];
|
||||
$files = [];
|
||||
|
||||
foreach ((array) scandir($realpath) as $name)
|
||||
{
|
||||
// Skip system locations
|
||||
if (empty($name) || $name == '.')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build destination path
|
||||
if (!$path = $this->_realpath($realpath . $name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate destination path
|
||||
if (!$this->valid($path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Context
|
||||
switch (true)
|
||||
{
|
||||
case is_dir($path):
|
||||
|
||||
$directories[] =
|
||||
[
|
||||
'file' => false,
|
||||
'name' => $name,
|
||||
'path' => $path,
|
||||
'time' => filemtime(
|
||||
$path
|
||||
)
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case is_file($path):
|
||||
|
||||
$files[] =
|
||||
[
|
||||
'file' => true,
|
||||
'name' => $name,
|
||||
'path' => $path,
|
||||
'time' => filemtime(
|
||||
$path
|
||||
)
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort order
|
||||
array_multisort(
|
||||
array_column(
|
||||
$directories,
|
||||
$sort
|
||||
),
|
||||
$order,
|
||||
$method,
|
||||
$directories
|
||||
);
|
||||
|
||||
// Sort files by name ASC
|
||||
array_multisort(
|
||||
array_column(
|
||||
$directories,
|
||||
$sort
|
||||
),
|
||||
$order,
|
||||
$method,
|
||||
$directories
|
||||
);
|
||||
|
||||
// Merge list
|
||||
return array_merge(
|
||||
$directories,
|
||||
$files
|
||||
);
|
||||
}
|
||||
|
||||
public function valid(?string $realpath): bool
|
||||
{
|
||||
if (empty($realpath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($realpath != $this->_realpath($realpath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!str_starts_with($realpath, $this->_root))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_contains($realpath, DIRECTORY_SEPARATOR . '.'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_readable($realpath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return absolute realpath with root constructed
|
||||
public function absolute(?string $path): ?string
|
||||
{
|
||||
if (!$realpath = $this->_realpath($this->_root . $path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $realpath;
|
||||
}
|
||||
|
||||
// PHP::realpath extension appending slash to dir paths
|
||||
private function _realpath(?string $path): ?string
|
||||
{
|
||||
if (empty($path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$realpath = realpath($path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_readable($realpath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_dir($realpath))
|
||||
{
|
||||
$realpath = rtrim(
|
||||
$realpath,
|
||||
DIRECTORY_SEPARATOR
|
||||
) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
return $realpath;
|
||||
}
|
||||
}
|
355
src/nex.php
355
src/nex.php
@ -1,355 +0,0 @@
|
||||
<?php
|
||||
|
||||
// Load dependencies
|
||||
require_once __DIR__ .
|
||||
DIRECTORY_SEPARATOR . '..'.
|
||||
DIRECTORY_SEPARATOR . 'vendor' .
|
||||
DIRECTORY_SEPARATOR . 'autoload.php';
|
||||
|
||||
// Parse startup arguments
|
||||
foreach ((array) $argv as $item)
|
||||
{
|
||||
if (preg_match('/^(?<key>[^=]+)=(?<value>.*)$/', $item, $argument))
|
||||
{
|
||||
switch ($argument['key'])
|
||||
{
|
||||
case 'host':
|
||||
|
||||
define(
|
||||
'NEXT_HOST',
|
||||
(string) $argument['value']
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'port':
|
||||
|
||||
define(
|
||||
'NEXT_PORT',
|
||||
(int) $argument['value']
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'path':
|
||||
|
||||
$path = rtrim(
|
||||
(string) $argument['value'],
|
||||
DIRECTORY_SEPARATOR
|
||||
) . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (!str_starts_with($path, DIRECTORY_SEPARATOR))
|
||||
{
|
||||
print(
|
||||
_('absolute path required')
|
||||
) . PHP_EOL;
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!is_dir($path) || !is_readable($path))
|
||||
{
|
||||
print(
|
||||
_('path not accessible')
|
||||
) . PHP_EOL;
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
define(
|
||||
'NEXT_PATH',
|
||||
(string) $path
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
|
||||
define(
|
||||
'NEXT_FILE',
|
||||
(string) $argument['value']
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'fail':
|
||||
|
||||
$fail = (string) $argument['value'];
|
||||
|
||||
if (!str_starts_with($fail, DIRECTORY_SEPARATOR))
|
||||
{
|
||||
print(
|
||||
_('absolute path required')
|
||||
) . PHP_EOL;
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!is_file($fail) || !is_readable($fail))
|
||||
{
|
||||
print(
|
||||
_('fail template not accessible')
|
||||
) . PHP_EOL;
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
define(
|
||||
'NEXT_FAIL',
|
||||
(string) file_get_contents(
|
||||
$fail
|
||||
)
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
|
||||
define(
|
||||
'NEXT_LIST',
|
||||
in_array(
|
||||
mb_strtolower(
|
||||
(string) $argument['value']
|
||||
),
|
||||
[
|
||||
'true',
|
||||
'yes',
|
||||
'1'
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'size':
|
||||
|
||||
define(
|
||||
'NEXT_SIZE',
|
||||
(int) $argument['value']
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'dump':
|
||||
|
||||
define(
|
||||
'NEXT_DUMP',
|
||||
(string) $argument['value']
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required arguments and set optional defaults
|
||||
if (!defined('NEXT_HOST')) define('NEXT_HOST', '127.0.0.1');
|
||||
|
||||
if (!defined('NEXT_PORT')) define('NEXT_PORT', 1900);
|
||||
|
||||
if (!defined('NEXT_PATH'))
|
||||
{
|
||||
print(
|
||||
_('path required')
|
||||
) . PHP_EOL;
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!defined('NEXT_FILE')) define('NEXT_FILE', false);
|
||||
|
||||
if (!defined('NEXT_LIST')) define('NEXT_LIST', true);
|
||||
|
||||
if (!defined('NEXT_SIZE')) define('NEXT_SIZE', 1024);
|
||||
|
||||
if (!defined('NEXT_FAIL')) define('NEXT_FAIL', 'fail');
|
||||
|
||||
if (!defined('NEXT_DUMP')) define('NEXT_DUMP', '[{time}] [{code}] {host}:{port} {path} {real} {size} bytes');
|
||||
|
||||
// Init server
|
||||
$server = new \Yggverse\Nex\Server(
|
||||
NEXT_HOST,
|
||||
NEXT_PORT,
|
||||
NEXT_SIZE
|
||||
);
|
||||
|
||||
$server->start(
|
||||
function (
|
||||
string $request,
|
||||
string $connect
|
||||
): ?string
|
||||
{
|
||||
// Define response
|
||||
$response = null;
|
||||
|
||||
// Filter request
|
||||
$request = trim(
|
||||
$request
|
||||
);
|
||||
|
||||
$request = empty($request) ? '/' : $request;
|
||||
|
||||
// Build realpath
|
||||
$realpath = realpath(
|
||||
NEXT_PATH .
|
||||
urldecode(
|
||||
filter_var(
|
||||
$request,
|
||||
FILTER_SANITIZE_URL
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Make sure directory path ending with slash
|
||||
if (is_dir($realpath))
|
||||
{
|
||||
$realpath = rtrim(
|
||||
$realpath,
|
||||
DIRECTORY_SEPARATOR
|
||||
) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
// Validate realpath exists, started with path defined and does not contain hidden entities
|
||||
if ($realpath && str_starts_with($realpath, NEXT_PATH) && !str_contains($realpath, DIRECTORY_SEPARATOR . '.'))
|
||||
{
|
||||
// Try directory
|
||||
if (is_dir($realpath))
|
||||
{
|
||||
// Try index file on enabled
|
||||
if (NEXT_FILE && file_exists($realpath . NEXT_FILE) && is_readable($realpath . NEXT_FILE))
|
||||
{
|
||||
// Update realpath returned on default file response
|
||||
$realpath = $realpath . NEXT_FILE;
|
||||
|
||||
$response = file_get_contents(
|
||||
$realpath
|
||||
);
|
||||
}
|
||||
|
||||
// Try directory listing on enabled
|
||||
else if (NEXT_LIST)
|
||||
{
|
||||
$directories = [];
|
||||
|
||||
$files = [];
|
||||
|
||||
foreach ((array) scandir($realpath) as $filename)
|
||||
{
|
||||
// Process system entities
|
||||
if (str_starts_with($filename, '.'))
|
||||
{
|
||||
// Parent navigation
|
||||
if ($filename == '..' && $parent = realpath($realpath . $filename))
|
||||
{
|
||||
$parent = rtrim(
|
||||
$parent,
|
||||
DIRECTORY_SEPARATOR
|
||||
) . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (str_starts_with($parent, NEXT_PATH))
|
||||
{
|
||||
$directories[$filename] = '=> ../';
|
||||
}
|
||||
}
|
||||
|
||||
continue; // skip everything else
|
||||
}
|
||||
|
||||
// Directory
|
||||
if (is_dir($realpath . $filename))
|
||||
{
|
||||
if (is_readable($realpath . $filename))
|
||||
{
|
||||
$directories[$filename] = sprintf(
|
||||
'=> %s/',
|
||||
urlencode(
|
||||
$filename
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// File
|
||||
if (is_readable($realpath . $filename))
|
||||
{
|
||||
$files[$filename] = sprintf(
|
||||
'=> %s',
|
||||
urlencode(
|
||||
$filename
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by keys ASC
|
||||
ksort(
|
||||
$directories,
|
||||
SORT_STRING | SORT_FLAG_CASE | SORT_NATURAL
|
||||
);
|
||||
|
||||
ksort(
|
||||
$files,
|
||||
SORT_STRING | SORT_FLAG_CASE | SORT_NATURAL
|
||||
);
|
||||
|
||||
// Merge items
|
||||
$response = implode(
|
||||
PHP_EOL,
|
||||
array_merge(
|
||||
$directories,
|
||||
$files
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try file
|
||||
else
|
||||
{
|
||||
$response = file_get_contents(
|
||||
$realpath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Dump request on enabled
|
||||
if (NEXT_DUMP)
|
||||
{
|
||||
// Build connection URL #72811
|
||||
$url = sprintf(
|
||||
'nex://%s',
|
||||
$connect
|
||||
);
|
||||
|
||||
// Print dump from template
|
||||
printf(
|
||||
str_ireplace(
|
||||
[
|
||||
'{time}',
|
||||
'{code}',
|
||||
'{host}',
|
||||
'{port}',
|
||||
'{path}',
|
||||
'{real}',
|
||||
'{size}'
|
||||
],
|
||||
[
|
||||
(string) date('c'),
|
||||
(string) (int) is_string($response),
|
||||
(string) parse_url($url, PHP_URL_HOST),
|
||||
(string) parse_url($url, PHP_URL_PORT),
|
||||
(string) str_replace('%', '%%', $request),
|
||||
(string) str_replace('%', '%%', empty($realpath) ? '!' : $realpath),
|
||||
(string) mb_strlen((string) $response)
|
||||
],
|
||||
NEXT_DUMP
|
||||
) . PHP_EOL
|
||||
);
|
||||
}
|
||||
|
||||
// Send response
|
||||
return is_string($response) ? $response : NEXT_FAIL;
|
||||
}
|
||||
);
|
59
src/server.php
Normal file
59
src/server.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
// Load dependencies
|
||||
require_once __DIR__ .
|
||||
DIRECTORY_SEPARATOR . '..'.
|
||||
DIRECTORY_SEPARATOR . 'vendor' .
|
||||
DIRECTORY_SEPARATOR . 'autoload.php';
|
||||
|
||||
// Init environment
|
||||
$environment = new \Yggverse\Next\Model\Environment(
|
||||
$argv,
|
||||
json_decode(
|
||||
file_get_contents(
|
||||
__DIR__ .
|
||||
DIRECTORY_SEPARATOR . '..'.
|
||||
DIRECTORY_SEPARATOR . 'default.json'
|
||||
),
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
// Init filesystem
|
||||
$filesystem = new \Yggverse\Next\Model\Filesystem(
|
||||
$environment->get('path')
|
||||
);
|
||||
|
||||
// Start server
|
||||
try
|
||||
{
|
||||
switch ($environment->get('type'))
|
||||
{
|
||||
case 'nex':
|
||||
|
||||
$server = \Ratchet\Server\IoServer::factory(
|
||||
new \Yggverse\Next\Controller\Nex(
|
||||
$environment,
|
||||
$filesystem
|
||||
),
|
||||
$environment->get('port'),
|
||||
$environment->get('host')
|
||||
);
|
||||
|
||||
$server->run();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
throw new \Exception(
|
||||
_('valid server type required!')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Show help
|
||||
catch (\Exception $exception)
|
||||
{
|
||||
// @TODO
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user