From 3dda92cfea5e50ba3b90478807ddf41690882d35 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:58:08 +0100 Subject: [PATCH 1/2] Add the possibility to bind `ServeCommand` to all interfaces --- formwork/src/Commands/ServeCommand.php | 59 ++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/formwork/src/Commands/ServeCommand.php b/formwork/src/Commands/ServeCommand.php index 354a65fea..de3992fe4 100644 --- a/formwork/src/Commands/ServeCommand.php +++ b/formwork/src/Commands/ServeCommand.php @@ -12,10 +12,20 @@ final class ServeCommand implements CommandInterface { + /** + * @var list List of loopback hosts + */ + private const array LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '::1']; + + /** + * @var list 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 @@ -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' => [ @@ -89,7 +99,8 @@ public function __invoke(?array $argv = null): never ], ]); - $this->climate->arguments->parse(); + // Ignore parsing errors to handle passing `--host` without a value as a flag to bind to all interfaces + @$this->climate->arguments->parse(); if ($this->climate->arguments->get('help')) { $this->climate->usage($argv); @@ -97,7 +108,9 @@ public function __invoke(?array $argv = null): never } /** @var string */ - $host = $this->climate->arguments->get('host'); + $host = $this->climate->arguments->defined('host') + ? ($this->climate->arguments->get('host') ?: '0.0.0.0') // Bind to all interfaces if `--host` is passed without a value + : $this->host; /** @var int */ $port = $this->climate->arguments->get('port'); @@ -164,6 +177,14 @@ private function handleOutput(string $type, array $lines): void $this->climate->br(); $this->climate->out(sprintf('➜ Listening on http://%s:%s/', $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: http://%s:%s/', $this->formatHost($localNetworkIp), $this->port)); + $this->climate->br(); + } + } + $this->climate->out('Press CTRL+C to stop'); $this->climate->br(); break; @@ -336,4 +357,34 @@ 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 + */ + private function getLocalNetworkIps(): array + { + 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 + ) { + $localNetworkIps[] = $data['address']; + } + } + } + + return $localNetworkIps; + } } From edc35bbb1e1dfe58ab92687faff935e39a42b350 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:21:31 +0200 Subject: [PATCH 2/2] Improve host handling --- formwork/src/Commands/ServeCommand.php | 32 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/formwork/src/Commands/ServeCommand.php b/formwork/src/Commands/ServeCommand.php index de3992fe4..746195e19 100644 --- a/formwork/src/Commands/ServeCommand.php +++ b/formwork/src/Commands/ServeCommand.php @@ -99,8 +99,12 @@ public function __invoke(?array $argv = null): never ], ]); - // Ignore parsing errors to handle passing `--host` without a value as a flag to bind to all interfaces - @$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); @@ -108,9 +112,12 @@ public function __invoke(?array $argv = null): never } /** @var string */ - $host = $this->climate->arguments->defined('host') - ? ($this->climate->arguments->get('host') ?: '0.0.0.0') // Bind to all interfaces if `--host` is passed without a value - : $this->host; + $host = $this->climate->arguments->get('host'); + + if (!$this->isValidHost($host)) { + $this->climate->to('error')->out(sprintf('Formwork %s Server failed to listen on invalid host %s', App::VERSION, $host)); + exit(1); + } /** @var int */ $port = $this->climate->arguments->get('port'); @@ -304,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 */ @@ -365,6 +381,12 @@ private function outputRawLine(string $type, string $line): void */ private function getLocalNetworkIps(): array { + if (!function_exists('net_get_interfaces')) { + $this->climate->to('error')->out('Cannot get local network IP addresses: function net_get_interfaces() is not available'); + $this->climate->br(); + return []; + } + if (($interfaces = net_get_interfaces()) === false) { return []; }