Skip to content

feat: add $allowedHostnames for multiple domain support #6785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
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
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ class App extends BaseConfig
*/
public string $baseURL = 'http://localhost:8080/';

/**
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
* If you want to accept multiple Hostnames, set this.
*
* E.g. When your site URL ($baseURL) is 'http://example.com/', and your site
* also accepts 'http://media.example.com/' and
* 'http://accounts.example.com/':
* ['media.example.com', 'accounts.example.com']
*
* @var string[]
* @phpstan-var list<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,12 @@ public function detectLocale($config)
* Sets up our URI object based on the information we have. This is
* either provided by the user in the baseURL Config setting, or
* determined from the environment as needed.
*
* @deprecated $protocol and $baseURL are deprecated. No longer used.
*/
protected function detectURI(string $protocol, string $baseURL)
{
// Passing the config is unnecessary but left for legacy purposes
$config = clone $this->config;
$config->baseURL = $baseURL;

$this->setPath($this->detectPath($protocol), $config);
$this->setPath($this->detectPath($this->config->uriProtocol), $this->config);
}

/**
Expand Down Expand Up @@ -270,7 +268,7 @@ protected function parseRequestURI(): string
}

// This section ensures that even on servers that require the URI to contain the query string (Nginx) a correct
// URI is found, and also fixes the QUERY_STRING getServer var and $_GET array.
// URI is found, and also fixes the QUERY_STRING Server var and $_GET array.
if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0) {
$query = explode('?', $query, 2);
$uri = $query[0];
Expand Down Expand Up @@ -400,19 +398,26 @@ public function setPath(string $path, ?App $config = null)

// It's possible the user forgot a trailing slash on their
// baseURL, so let's help them out.
$baseURL = $config->baseURL === '' ? $config->baseURL : rtrim($config->baseURL, '/ ') . '/';
$baseURL = ($config->baseURL === '') ? $config->baseURL : rtrim($config->baseURL, '/ ') . '/';

// Based on our baseURL provided by the developer
// set our current domain name, scheme
// Based on our baseURL and allowedHostnames provided by the developer
// and HTTP_HOST, set our current domain name, scheme.
if ($baseURL !== '') {
$host = $this->determineHost($config, $baseURL);

// Set URI::$baseURL
$uri = new URI($baseURL);
$currentBaseURL = (string) $uri->setHost($host);
$this->uri->setBaseURL($currentBaseURL);

$this->uri->setScheme(parse_url($baseURL, PHP_URL_SCHEME));
$this->uri->setHost(parse_url($baseURL, PHP_URL_HOST));
$this->uri->setHost($host);
$this->uri->setPort(parse_url($baseURL, PHP_URL_PORT));

// Ensure we have any query vars
$this->uri->setQuery($_SERVER['QUERY_STRING'] ?? '');

// Check if the baseURL scheme needs to be coerced into its secure version
// Check if the scheme needs to be coerced into its secure version
if ($config->forceGlobalSecureRequests && $this->uri->getScheme() === 'http') {
$this->uri->setScheme('https');
}
Expand All @@ -425,6 +430,27 @@ public function setPath(string $path, ?App $config = null)
return $this;
}

private function determineHost(App $config, string $baseURL): string
{
$host = parse_url($baseURL, PHP_URL_HOST);

if (empty($config->allowedHostnames)) {
return $host;
}

// Update host if it is valid.
$httpHostPort = $this->getServer('HTTP_HOST');
if ($httpHostPort !== null) {
[$httpHost] = explode(':', $httpHostPort, 2);

if (in_array($httpHost, $config->allowedHostnames, true)) {
$host = $httpHost;
}
}

return $host;
}

/**
* Returns the path relative to SCRIPT_NAME,
* running detection as necessary.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All @@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand Down Expand Up @@ -83,6 +88,11 @@ class URI
/**
* URI path.
*
* Note: The constructor of the IncomingRequest class changes the path of
* the URI object held by the IncomingRequest class to a path relative
* to the SCRIPT_NAME. If the baseURL contains subfolders, this value
* will be different from the current URI path.
*
* @var string
*/
protected $path;
Expand Down Expand Up @@ -232,9 +242,12 @@ public static function removeDotSegments(string $path): string
/**
* Constructor.
*
* @param string $uri
* @param string|null $uri The URI to parse.
*
* @throws HTTPException
*
* @throws InvalidArgumentException
* @TODO null for param $uri should be removed.
* See https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface
*/
public function __construct(?string $uri = null)
{
Expand Down Expand Up @@ -273,6 +286,8 @@ public function useRawQueryString(bool $raw = true)
* Sets and overwrites any current URI information.
*
* @return URI
*
* @throws HTTPException
*/
public function setURI(?string $uri = null)
{
Expand Down Expand Up @@ -744,6 +759,30 @@ public function setPath(string $path)
return $this;
}

/**
* Sets the current baseURL.
*
* @interal
*/
public function setBaseURL(string $baseURL): void
{
$this->baseURL = $baseURL;
}

/**
* Returns the current baseURL.
*
* @interal
*/
public function getBaseURL(): string
{
if ($this->baseURL === null) {
throw new BadMethodCallException('The $baseURL is not set.');
}

return $this->baseURL;
}

/**
* Sets the path portion of the URI based on segments.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* the LICENSE file that was distributed with this source code.
*/

use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Router\Exceptions\RouterException;
Expand All @@ -19,14 +21,15 @@

if (! function_exists('_get_uri')) {
/**
* Used by the other URL functions to build a
* framework-specific URI based on the App config.
* Used by the other URL functions to build a framework-specific URI
* based on $request->getUri()->getBaseURL() and the App config.
*
* @internal Outside of the framework this should not be used directly.
* @internal Outside the framework this should not be used directly.
*
* @param string $relativePath May include queries or fragments
*
* @throws InvalidArgumentException For invalid paths or config
* @throws HTTPException For invalid paths.
* @throws InvalidArgumentException For invalid config.
*/
function _get_uri(string $relativePath = '', ?App $config = null): URI
{
Expand All @@ -37,7 +40,7 @@ function _get_uri(string $relativePath = '', ?App $config = null): URI
}

// If a full URI was passed then convert it
if (is_int(strpos($relativePath, '://'))) {
if (strpos($relativePath, '://') !== false) {
$full = new URI($relativePath);
$relativePath = URI::createURIString(
null,
Expand All @@ -51,7 +54,14 @@ function _get_uri(string $relativePath = '', ?App $config = null): URI
$relativePath = URI::removeDotSegments($relativePath);

// Build the full URL based on $config and $relativePath
$url = rtrim($config->baseURL, '/ ') . '/';
$request = Services::request();

if ($request instanceof CLIRequest) {
/** @var App $config */
$url = rtrim($config->baseURL, '/ ') . '/';
} else {
$url = $request->getUri()->getBaseURL();
}

// Check for an index page
if ($config->indexPage !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All @@ -48,7 +50,12 @@ protected function setUp(): void
$this->routes = new RouteCollection(Services::locator(), new Modules());
Services::injectMock('routes', $this->routes);

$this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent());
$this->request = new MockIncomingRequest(
$this->config,
new URI('http://example.com'),
null,
new UserAgent()
);
Services::injectMock('request', $this->request);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand Down Expand Up @@ -164,6 +164,8 @@ public function testSetLink()
$config->baseURL = 'http://example.com/test/';
Factories::injectMock('config', 'App', $config);

$this->resetServices();

$response = new Response($config);
$pager = Services::pager();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,38 @@ protected function tearDown(): void

public function testCurrentURLReturnsBasicURL()
{
// Since we're on a CLI, we must provide our own URI
$_SERVER['REQUEST_URI'] = '/public';
$_SERVER['SCRIPT_NAME'] = '/public/index.php';

$this->config->baseURL = 'http://example.com/public';

$this->assertSame('http://example.com/public/index.php/', current_url());
}

public function testCurrentURLReturnsAllowedHostname()
{
$_SERVER['HTTP_HOST'] = 'www.example.jp';
$_SERVER['REQUEST_URI'] = '/public';
$_SERVER['SCRIPT_NAME'] = '/public/index.php';

$this->config->baseURL = 'http://example.com/public';
$this->config->allowedHostnames = ['www.example.jp'];

$this->assertSame('http://www.example.jp/public/index.php/', current_url());
}

public function testCurrentURLReturnsBaseURLIfNotAllowedHostname()
{
$_SERVER['HTTP_HOST'] = 'invalid.example.org';
$_SERVER['REQUEST_URI'] = '/public';
$_SERVER['SCRIPT_NAME'] = '/public/index.php';

$this->config->baseURL = 'http://example.com/public';
$this->config->allowedHostnames = ['www.example.jp'];

$this->assertSame('http://example.com/public/index.php/', current_url());
}

public function testCurrentURLReturnsObject()
{
// Since we're on a CLI, we must provide our own URI
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,49 @@ public function testBaseURLService()
$_SERVER['HTTP_HOST'] = 'example.com';
$_SERVER['REQUEST_URI'] = '/ci/v4/x/y';

$uri = new URI('http://example.com/ci/v4/x/y');
Services::injectMock('uri', $uri);

$this->config->baseURL = 'http://example.com/ci/v4/';
$request = Services::request($this->config);
Services::injectMock('request', $request);

$this->assertSame('http://example.com/ci/v4/index.php/controller/method', site_url('controller/method', null, $this->config));
$this->assertSame('http://example.com/ci/v4/controller/method', base_url('controller/method', null));
}

public function testSiteURLWithAllowedHostname()
{
$_SERVER['HTTP_HOST'] = 'www.example.jp';
$_SERVER['REQUEST_URI'] = '/public';
$_SERVER['SCRIPT_NAME'] = '/public/index.php';

$this->config->baseURL = 'http://example.com/public/';
$this->config->allowedHostnames = ['www.example.jp'];

// URI object are updated in IncomingRequest constructor.
$request = Services::incomingrequest($this->config);
Services::injectMock('request', $request);

$this->assertSame(
'http://www.example.jp/public/index.php/controller/method',
site_url('controller/method', null, $this->config)
);
}

public function testBaseURLWithAllowedHostname()
{
$_SERVER['HTTP_HOST'] = 'www.example.jp';
$_SERVER['REQUEST_URI'] = '/public';
$_SERVER['SCRIPT_NAME'] = '/public/index.php';

$this->config->baseURL = 'http://example.com/public/';
$this->config->allowedHostnames = ['www.example.jp'];

// URI object are updated in IncomingRequest constructor.
$request = Services::incomingrequest($this->config);
Services::injectMock('request', $request);

$this->assertSame(
'http://www.example.jp/public/controller/method',
base_url('controller/method', null)
);
}
}
Loading