Added minimal webhook http server.
This commit is contained in:
62
src/Commands/System/ServerCommand.php
Normal file
62
src/Commands/System/ServerCommand.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Commands\System;
|
||||
|
||||
use App\Command;
|
||||
use App\Libs\Routable;
|
||||
use App\Libs\Server;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[Routable(command: self::ROUTE)]
|
||||
final class ServerCommand extends Command
|
||||
{
|
||||
public const ROUTE = 'system:server';
|
||||
|
||||
public function __construct(private Server $server)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::ROUTE)
|
||||
->setDescription('Start minimal http server.')
|
||||
->addOption('interface', 'i', InputOption::VALUE_REQUIRED, 'Bind to interface.', '0.0.0.0')
|
||||
->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'Bind to port.', 8080)
|
||||
->addOption('threads', 't', InputOption::VALUE_REQUIRED, 'How many threads to use.', 1)
|
||||
->setHelp(
|
||||
<<<HELP
|
||||
|
||||
This server is not meant to be used in production. It is mainly for testing purposes.
|
||||
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
protected function runCommand(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$host = $input->getOption('interface');
|
||||
$port = (int)$input->getOption('port');
|
||||
$threads = (int)$input->getOption('threads');
|
||||
|
||||
$this->server = $this->server->withInterface($host)->withPort($port)->withThreads($threads)
|
||||
->runInBackground(
|
||||
fn($std, $out) => $output->writeln(trim($out), OutputInterface::VERBOSITY_VERBOSE)
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
r('Listening on \'http://{host}:{port}\' for webhook events.', [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
])
|
||||
);
|
||||
|
||||
$this->server->wait();
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
411
src/Libs/Server.php
Normal file
411
src/Libs/Server.php
Normal file
@@ -0,0 +1,411 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libs;
|
||||
|
||||
use Closure;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class Server
|
||||
{
|
||||
public const CONFIG_HOST = 'host';
|
||||
public const CONFIG_PORT = 'port';
|
||||
public const CONFIG_ROOT = 'root';
|
||||
public const CONFIG_PHP = 'php';
|
||||
public const CONFIG_ENV = 'env';
|
||||
public const CONFIG_ROUTER = 'router';
|
||||
public const CONFIG_THREADS = 'threads';
|
||||
|
||||
/**
|
||||
* The default and user merged configuration array
|
||||
*/
|
||||
private array $config = [];
|
||||
|
||||
/**
|
||||
* Indicate whether the server is currently running
|
||||
*/
|
||||
private bool $running = false;
|
||||
|
||||
/**
|
||||
* The initiated process.
|
||||
*/
|
||||
private Process|null $process = null;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$classExists = class_exists(PhpExecutableFinder::class);
|
||||
|
||||
$this->config = [
|
||||
self::CONFIG_HOST => '0.0.0.0',
|
||||
self::CONFIG_PORT => 8080,
|
||||
self::CONFIG_ROUTER => null,
|
||||
self::CONFIG_ROOT => realpath(__DIR__ . '/../../public'),
|
||||
self::CONFIG_PHP => $classExists ? (new PhpExecutableFinder())->find(false) : PHP_BINARY,
|
||||
self::CONFIG_ENV => array_replace_recursive($_ENV, getenv()),
|
||||
self::CONFIG_THREADS => 1,
|
||||
];
|
||||
|
||||
if (null !== ($config[self::CONFIG_HOST] ?? null)) {
|
||||
$this->withInterface($config[self::CONFIG_HOST]);
|
||||
$this->config[self::CONFIG_HOST] = $config[self::CONFIG_HOST];
|
||||
}
|
||||
|
||||
if (null !== ($config[self::CONFIG_PORT] ?? null)) {
|
||||
$this->withPort($config[self::CONFIG_PORT]);
|
||||
$this->config[self::CONFIG_PORT] = $config[self::CONFIG_PORT];
|
||||
}
|
||||
|
||||
if (null !== ($config[self::CONFIG_ROUTER] ?? null)) {
|
||||
$this->withRouter($config[self::CONFIG_ROUTER]);
|
||||
$this->config[self::CONFIG_ROUTER] = $config[self::CONFIG_ROUTER];
|
||||
}
|
||||
|
||||
if (null !== ($config[self::CONFIG_ROOT] ?? null)) {
|
||||
$this->withRoot($config[self::CONFIG_ROOT]);
|
||||
$this->config[self::CONFIG_ROOT] = $config[self::CONFIG_ROOT];
|
||||
}
|
||||
|
||||
if (null !== ($config[self::CONFIG_PHP] ?? null)) {
|
||||
$this->withPHP($config[self::CONFIG_PHP]);
|
||||
$this->config[self::CONFIG_PHP] = $config[self::CONFIG_PHP];
|
||||
}
|
||||
|
||||
if (null !== ($config[self::CONFIG_THREADS] ?? null)) {
|
||||
$this->withThreads($config[self::CONFIG_THREADS]);
|
||||
$this->config[self::CONFIG_THREADS] = $config[self::CONFIG_THREADS];
|
||||
}
|
||||
|
||||
if (null !== ($config[self::CONFIG_ENV] ?? null)) {
|
||||
$this->withENV($config[self::CONFIG_ENV]);
|
||||
$this->config[self::CONFIG_ENV] = array_replace_recursive(
|
||||
$this->config[self::CONFIG_ENV],
|
||||
$config[self::CONFIG_ENV]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Path to PHP binary.
|
||||
*
|
||||
* @param string $php
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withPHP(string $php): self
|
||||
{
|
||||
if (false === is_executable($php)) {
|
||||
throw new RuntimeException(sprintf('PHP binary \'%s\' is not executable.', $php));
|
||||
}
|
||||
|
||||
if ($this->config[self::CONFIG_PHP] === $php) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$instance = clone $this;
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_PHP] = $php;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Host to bind to.
|
||||
*
|
||||
* @param string $host
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withInterface(string $host): self
|
||||
{
|
||||
if ($this->config[self::CONFIG_HOST] === $host) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$instance = clone $this;
|
||||
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_HOST] = $host;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Port.
|
||||
*
|
||||
* @param int $port
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withPort(int $port): self
|
||||
{
|
||||
if ($this->config[self::CONFIG_PORT] === $port) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$instance = clone $this;
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_PORT] = $port;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set How many threads to use.
|
||||
*
|
||||
* @param int $threads
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withThreads(int $threads): self
|
||||
{
|
||||
if ($this->config[self::CONFIG_THREADS] === $threads) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$instance = clone $this;
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_THREADS] = $threads;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Root path.
|
||||
*
|
||||
* @param string $root
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withRoot(string $root): self
|
||||
{
|
||||
if (!is_dir($root)) {
|
||||
throw new RuntimeException(sprintf('Root path \'%s\' is not a directory.', $root));
|
||||
}
|
||||
|
||||
if ($this->config[self::CONFIG_ROOT] === $root) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$instance = clone $this;
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_ROOT] = $root;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set PHP Router file.
|
||||
*
|
||||
* @param string $router
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withRouter(string $router): self
|
||||
{
|
||||
if (false === file_exists($router)) {
|
||||
throw new RuntimeException(sprintf('The router file \'%s\' does not exist.', $router));
|
||||
}
|
||||
|
||||
if ($this->config[self::CONFIG_ROUTER] === $router) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$instance = clone $this;
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_ROUTER] = $router;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Environment variables.
|
||||
*
|
||||
* @param array $vars key/value pair.
|
||||
* @param bool $clear Clear Currently loaded environment.
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withENV(array $vars, bool $clear = false): self
|
||||
{
|
||||
$instance = clone $this;
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_ENV] = array_replace_recursive(
|
||||
false === $clear ? $instance->config[self::CONFIG_ENV] : [],
|
||||
$vars
|
||||
);
|
||||
|
||||
return $instance->config[self::CONFIG_ENV] === $this->config[self::CONFIG_ENV] ? $this : $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude environment variables from loaded list.
|
||||
*
|
||||
* @param array $vars
|
||||
*
|
||||
* @return $this cloned instance.
|
||||
*/
|
||||
public function withoutENV(array $vars): self
|
||||
{
|
||||
$instance = clone $this;
|
||||
|
||||
$instance->process = null;
|
||||
$instance->running = false;
|
||||
|
||||
$instance->config[self::CONFIG_ENV] = array_filter(
|
||||
$instance->config[self::CONFIG_ENV],
|
||||
fn($key) => false === in_array($key, $vars),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
return $instance->config[self::CONFIG_ENV] === $this->config[self::CONFIG_ENV] ? $this : $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Server in blocking mode.
|
||||
*/
|
||||
public function run(Closure|null $output = null): int
|
||||
{
|
||||
$this->process = $this->makeServer($output);
|
||||
|
||||
return $this->process->wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hang around until the server is killed.
|
||||
*/
|
||||
public function wait(): int
|
||||
{
|
||||
if (false === $this->isRunning()) {
|
||||
throw new RuntimeException('No server was started.');
|
||||
}
|
||||
|
||||
return $this->process->wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run server in background.
|
||||
*/
|
||||
public function runInBackground(Closure|null $output = null): self
|
||||
{
|
||||
$this->process = $this->makeServer($output);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop currently running server.
|
||||
*
|
||||
* @param int $timeout kill process if it does not exist in given seconds.
|
||||
* @param int|null $signal stop signal.
|
||||
*
|
||||
* @return int return 20002 if the server is not running. otherwise process exit code will be returned.
|
||||
*/
|
||||
public function stop(int $timeout = 10, int|null $signal = null): int
|
||||
{
|
||||
if (null === $this->process) {
|
||||
return 20002;
|
||||
}
|
||||
|
||||
$this->process->stop($timeout, $signal);
|
||||
|
||||
$this->running = false;
|
||||
|
||||
return $this->process->getExitCode();
|
||||
}
|
||||
|
||||
public function getInterface(): string
|
||||
{
|
||||
return $this->config[self::CONFIG_HOST];
|
||||
}
|
||||
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->config[self::CONFIG_PORT];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the process is running.
|
||||
*/
|
||||
public function isRunning(): bool
|
||||
{
|
||||
return $this->running;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Get loaded configuration.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Process|null Return server process or null if not initiated yet.
|
||||
*/
|
||||
public function getProcess(): Process|null
|
||||
{
|
||||
return $this->process;
|
||||
}
|
||||
|
||||
private function buildServeCommand(): array
|
||||
{
|
||||
$command = [
|
||||
$this->config[self::CONFIG_PHP],
|
||||
'-S',
|
||||
$this->config[self::CONFIG_HOST] . ':' . $this->config[self::CONFIG_PORT],
|
||||
'-t',
|
||||
$this->config[self::CONFIG_ROOT],
|
||||
];
|
||||
|
||||
if (null !== $this->config[self::CONFIG_ROUTER]) {
|
||||
$command[] = $this->config[self::CONFIG_ROUTER];
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
private function makeServer(Closure|null $output = null): Process
|
||||
{
|
||||
$env = $this->config[self::CONFIG_ENV];
|
||||
|
||||
if (null !== ($this->config[self::CONFIG_THREADS] ?? null) && $this->config[self::CONFIG_THREADS] > 1) {
|
||||
$env['PHP_CLI_SERVER_WORKERS'] = $this->config[self::CONFIG_THREADS];
|
||||
}
|
||||
|
||||
$process = new Process(
|
||||
command: $this->buildServeCommand(),
|
||||
env: $env,
|
||||
timeout: null
|
||||
);
|
||||
|
||||
$process->start($output);
|
||||
|
||||
$this->running = $process->isRunning();
|
||||
|
||||
return $process;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (true === $this->isRunning()) {
|
||||
$this->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
216
tests/Server/ServerTest.php
Normal file
216
tests/Server/ServerTest.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Server;
|
||||
|
||||
use App\Libs\Server;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class ServerTest extends TestCase
|
||||
{
|
||||
private array $config = [];
|
||||
private Server|null $server = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
try {
|
||||
$randomPort = random_int(50000, 65535);
|
||||
} catch (Exception) {
|
||||
$randomPort = 24587;
|
||||
}
|
||||
|
||||
$this->config = [
|
||||
Server::CONFIG_HOST => '0.0.0.0',
|
||||
Server::CONFIG_PORT => $randomPort,
|
||||
Server::CONFIG_PHP => PHP_BINARY,
|
||||
Server::CONFIG_THREADS => 1,
|
||||
Server::CONFIG_ROOT => __DIR__,
|
||||
Server::CONFIG_ROUTER => __FILE__,
|
||||
Server::CONFIG_ENV => [
|
||||
'test_a' => 1,
|
||||
'test_b' => 2,
|
||||
],
|
||||
];
|
||||
|
||||
$this->server = new Server($this->config);
|
||||
$this->server = $this->server->withENV($this->config[Server::CONFIG_ENV], true);
|
||||
}
|
||||
|
||||
public function test_constructor_conditions(): void
|
||||
{
|
||||
$config = [
|
||||
Server::CONFIG_HOST => '0.0.0.1',
|
||||
Server::CONFIG_PORT => 8081,
|
||||
Server::CONFIG_ROUTER => __FILE__,
|
||||
Server::CONFIG_ROOT => __DIR__,
|
||||
Server::CONFIG_PHP => PHP_BINARY,
|
||||
];
|
||||
|
||||
$server = new Server($config);
|
||||
|
||||
$this->assertEquals(
|
||||
array_intersect_key($server->getConfig(), $config),
|
||||
$config,
|
||||
'Should be equal config.'
|
||||
);
|
||||
|
||||
$c = (new Server())->getConfig();
|
||||
|
||||
$this->assertSame($c[Server::CONFIG_HOST], '0.0.0.0', 'Default host has changed.');
|
||||
$this->assertSame($c[Server::CONFIG_PORT], 8080, 'Default port has changed.');
|
||||
$this->assertSame($c[Server::CONFIG_THREADS], 1, 'Default threads changed.');
|
||||
$this->assertSame($c[Server::CONFIG_ROOT], realpath(__DIR__ . '/../../public'), 'Default root has changed.');
|
||||
}
|
||||
|
||||
public function test_withPHP_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withPHP($this->config[Server::CONFIG_PHP]),
|
||||
'Should be same object.'
|
||||
);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->server->withPHP($this->config[Server::CONFIG_PHP] . 'a');
|
||||
}
|
||||
|
||||
public function test_withInterface_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withInterface($this->config[Server::CONFIG_HOST]),
|
||||
'Should return same object.'
|
||||
);
|
||||
|
||||
$this->assertNotSame(
|
||||
$this->server,
|
||||
$this->server->withInterface('0.0.0.1'),
|
||||
'Should be different object.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_getInterface_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->config[Server::CONFIG_HOST],
|
||||
$this->server->getInterface(),
|
||||
'Should be the same.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_withPort_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withPort($this->config[Server::CONFIG_PORT]),
|
||||
'should return same object.'
|
||||
);
|
||||
|
||||
$this->assertNotSame(
|
||||
$this->server,
|
||||
$this->server->withPort($this->config[Server::CONFIG_PORT] + 1),
|
||||
'Should not be same object.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_getPort_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->config[Server::CONFIG_PORT],
|
||||
$this->server->getPort(),
|
||||
'Should be the same.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_withThreads_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withThreads($this->config[Server::CONFIG_THREADS]),
|
||||
'Should return same object. As threads has not changed.'
|
||||
);
|
||||
|
||||
$this->assertNotSame(
|
||||
$this->server,
|
||||
$this->server->withThreads($this->config[Server::CONFIG_THREADS] + 1),
|
||||
'Should return new object. As we have changed threads number.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_withRoot_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withRoot($this->config[Server::CONFIG_ROOT]),
|
||||
'Should return same object.'
|
||||
);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->server->withRoot($this->config[Server::CONFIG_ROOT] . $this->config[Server::CONFIG_ROOT]);
|
||||
}
|
||||
|
||||
public function test_withRouter_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withRouter($this->config[Server::CONFIG_ROUTER])
|
||||
);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->server->withRouter($this->config[Server::CONFIG_ROUTER] . 'zzz');
|
||||
}
|
||||
|
||||
public function test_withENV_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withENV(['test_b' => 2]),
|
||||
'Supposed to return same object. As value did not change.'
|
||||
);
|
||||
|
||||
$this->assertNotSame(
|
||||
$this->server,
|
||||
$this->server->withENV(['foo' => 'bar']),
|
||||
'Not supposed to return same object with changed value.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_withoutENV_conditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
$this->server,
|
||||
$this->server->withoutENV(['test_non_existent_env']),
|
||||
'Supposed to return same object. as key does not exists'
|
||||
);
|
||||
|
||||
$this->assertNotSame(
|
||||
$this->server,
|
||||
$this->server->withoutENV(['test_a']),
|
||||
'Not supposed to return same object with changed value.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
['test_a' => 1],
|
||||
$this->server->withoutENV(['test_b'])->getConfig()[Server::CONFIG_ENV],
|
||||
'Should be identical array.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_getProcess_conditions(): void
|
||||
{
|
||||
$this->assertNull($this->server->getProcess(), 'Should be null at this point');
|
||||
|
||||
$server = $this->server->withThreads(1)
|
||||
->withRoot(__DIR__ . '/resources')
|
||||
->withRouter(__DIR__ . '/resources/index.php')
|
||||
->runInBackground();
|
||||
|
||||
$this->assertInstanceOf(Process::class, $server->getProcess());
|
||||
|
||||
$server->stop();
|
||||
}
|
||||
}
|
||||
12
tests/Server/resources/index.html
Normal file
12
tests/Server/resources/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
3
tests/Server/resources/index.php
Normal file
3
tests/Server/resources/index.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
echo 'HelloTester';
|
||||
Reference in New Issue
Block a user