Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 76 additions & 3 deletions formwork/src/Commands/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@

final class ServeCommand implements CommandInterface
{
/**
* @var list<string> List of loopback hosts
*/
private const array LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '::1'];

/**
* @var list<string> List of wildcard hosts
*/
private const array WILDCARD_HOSTS = ['0.0.0.0', '::'];

/**
* Host to bind the server to
*/
private string $host = '127.0.0.1';
private string $host = 'localhost';

/**
* Port to bind the server to
Expand Down Expand Up @@ -66,7 +76,7 @@ public function __invoke(?array $argv = null): never
$this->climate->arguments->add([
'host' => [
'longPrefix' => 'host',
'description' => 'Host to bind the server to',
'description' => 'Host to bind the server to (if the value is omitted, the server will bind to all interfaces)',
'defaultValue' => $this->host,
],
'port' => [
Expand All @@ -89,7 +99,12 @@ public function __invoke(?array $argv = null): never
],
]);

$this->climate->arguments->parse();
// --host without a value binds to all interfaces
foreach (array_keys($argv, '--host', true) as $key) {
$argv[$key] = '--host=0.0.0.0';
}

$this->climate->arguments->parse($argv);

if ($this->climate->arguments->get('help')) {
$this->climate->usage($argv);
Expand All @@ -99,6 +114,11 @@ public function __invoke(?array $argv = null): never
/** @var string */
$host = $this->climate->arguments->get('host');

if (!$this->isValidHost($host)) {
$this->climate->to('error')->out(sprintf('<bold>Formwork <cyan>%s</cyan></bold> Server <red>failed to listen on invalid host <bold>%s</bold></red>', App::VERSION, $host));
exit(1);
}

/** @var int */
$port = $this->climate->arguments->get('port');

Expand Down Expand Up @@ -164,6 +184,14 @@ private function handleOutput(string $type, array $lines): void
$this->climate->br();
$this->climate->out(sprintf('➜ Listening on <cyan>http://%s:<bold>%s</bold>/</cyan>', $this->formatHost($this->host), $this->port));
$this->climate->br();

if (in_array($this->host, self::WILDCARD_HOSTS, true)) {
foreach ($this->getLocalNetworkIps() as $localNetworkIp) {
$this->climate->out(sprintf('➜ Remote address: <cyan>http://%s:<bold>%s</bold>/</cyan>', $this->formatHost($localNetworkIp), $this->port));
$this->climate->br();
}
}

$this->climate->out('<dark_gray>Press <bold>CTRL+C</bold> to stop</dark_gray>');
$this->climate->br();
break;
Expand Down Expand Up @@ -283,6 +311,15 @@ private function colorStatus(int $status): string
throw new UnexpectedValueException(sprintf('Unexpected status code %d', $status));
}

/**
* Check if host is a valid IP address or hostname
*/
private function isValidHost(string $host): bool
{
return filter_var($host, FILTER_VALIDATE_IP) !== false
|| filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
}

/**
* Format host for display and binding
*/
Expand Down Expand Up @@ -336,4 +373,40 @@ private function outputRawLine(string $type, string $line): void
throw new UnexpectedValueException(sprintf('Unexpected output type "%s"', $type));
}
}

/**
* Get local network IP addresses, excluding loopback interfaces
*
* @return list<string>
*/
private function getLocalNetworkIps(): array
{
if (!function_exists('net_get_interfaces')) {
$this->climate->to('error')->out('<yellow>Cannot get local network IP addresses: function net_get_interfaces() is not available</yellow>');
$this->climate->br();
return [];
}

if (($interfaces = net_get_interfaces()) === false) {
return [];
}

$localNetworkIps = [];

foreach ($interfaces as $interface) {
if (!isset($interface['unicast'])) {
continue;
}
foreach ($interface['unicast'] as $data) {
if (
$data['family'] === AF_INET // IPv4 addresses only
&& !in_array($data['address'], self::LOOPBACK_HOSTS, true) // Exclude loopback address
) {
Comment thread
giuscris marked this conversation as resolved.
$localNetworkIps[] = $data['address'];
}
}
}

return $localNetworkIps;
}
}
Loading