問題
サイト自体は https でアクセス出来ていて、.env の APP_URL にはちゃんと https 始まりの URL を設定している。
この状態で
route('admin.home') // ---> http://localhost/admin
config('app.url') // ---> https://localhost
と、なぜか route() ヘルパーのときだけ http になってしまい、Mixed Content が発生して正常に動作しなくなる。
config:cache とか Laravel の cache 機能は使っていない。
一応以下コマンドでクリアしてみても結果は変わらない。
$ php artisan cache:clear && php artisan view:clear && php artisan config:clear
route ヘルパーの処理を追って、何が原因で http 始まりの URL を返しているか調査
route() ヘルパーはどんなことをしていて、どこの情報を参照しているのか?を地道に追っていく。
vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
function route($name, $parameters = [], $absolute = true)
{
return app('url')->route($name, $parameters, $absolute);
}
app('url') は Illuminate/Routing/UrlGenerator を返す。
vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
public function route($name, $parameters = [], $absolute = true)
{
if (! is_null($route = $this->routes->getByName($name))) {
return $this->toRoute($route, $parameters, $absolute);
}
throw new RouteNotFoundException("Route [{$name}] not defined.");
}
public function toRoute($route, $parameters, $absolute)
{
$parameters = collect(Arr::wrap($parameters))->map(function ($value, $key) use ($route) {
$value = $value instanceof UrlRoutable && $route->bindingFieldFor($key)
? $value->{$route->bindingFieldFor($key)}
: $value;
return function_exists('enum_exists') && $value instanceof BackedEnum
? $value->value
: $value;
})->all();
return $this->routeUrl()->to(
$route, $this->formatParameters($parameters), $absolute
);
}
$this->routeUrl() は Illuminate/Routing/RouteUrlGenerator を返す。
vendor/laravel/framework/src/Illuminate/Routing/RouteUrlGenerator.php
public function to($route, $parameters = [], $absolute = false)
{
$domain = $this->getRouteDomain($route, $parameters);
// First we will construct the entire URI including the root and query string. Once it
// has been constructed, we'll make sure we don't have any missing parameters or we
// will need to throw the exception to let the developers know one was not given.
$uri = $this->addQueryString($this->url->format(
$root = $this->replaceRootParameters($route, $domain, $parameters),
$this->replaceRouteParameters($route->uri(), $parameters),
$route
), $parameters);
if (preg_match_all('/{(.*?)}/', $uri, $matchedMissingParameters)) {
throw UrlGenerationException::forMissingParameters($route, $matchedMissingParameters[1]);
}
// Once we have ensured that there are no missing parameters in the URI we will encode
// the URI and prepare it for returning to the developer. If the URI is supposed to
// be absolute, we will return it as-is. Otherwise we will remove the URL's root.
$uri = strtr(rawurlencode($uri), $this->dontEncode);
if (! $absolute) {
$uri = preg_replace('#^(//|[^/?])+#', '', $uri);
if ($base = $this->request->getBaseUrl()) {
$uri = preg_replace('#^'.$base.'#i', '', $uri);
}
return '/'.ltrim($uri, '/');
}
return $uri;
}
$this->url は Illuminate/Routing/UrlGenerator を返す。
vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
dump() でインスタンスの情報を確認したときに、ここで初めて http で始まっているアプリケーション URL を見つけた。
こいつが原因ぽい。
^ Illuminate\Routing\UrlGenerator {#198 ▼
:
#cachedRoot: "http://localhost"
以下で cachedRoot が設定される。
public function formatRoot($scheme, $root = null)
{
if (is_null($root)) {
if (is_null($this->cachedRoot)) {
$this->cachedRoot = $this->forcedRoot ?: $this->request->root();
}
$root = $this->cachedRoot;
}
$start = str_starts_with($root, 'http://') ? 'http://' : 'https://';
return preg_replace('~'.$start.'~', $scheme, $root, 1);
}
$this->request->root() を追う。
$this->forcedRoot // --> null
$this->request->root() // --> http://localhost/admin
$this->request->root() は Illuminate\Http\Request を返す。
vendor/laravel/framework/src/Illuminate/Http/Request.php
public function root()
{
return rtrim($this->getSchemeAndHttpHost().$this->getBaseUrl(), '/');
}
$this->getSchemeAndHttpHost() が http 始まりの URL だった。
親クラスの Symfony の Request クラス Symfony\Component\HttpFoundation\Request で定義されている。
vendor/symfony/http-foundation/Request.php
public function getSchemeAndHttpHost(): string
{
return $this->getScheme().'://'.$this->getHttpHost();
}
$this->getScheme() が http を返す。
public function getScheme(): string
{
return $this->isSecure() ? 'https' : 'http';
}
public function isSecure(): bool
{
if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) {
return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true);
}
$https = $this->server->get('HTTPS');
return !empty($https) && 'off' !== strtolower($https);
}
isSecure の後半の return 側。
$https = $this->server->get('HTTPS');
の値を最終的に返却している。
var_dump($_SERVER['HTTPS'] ?? 'undefined'); // --> undefined
$_SERVER['HTTPS'] が設定されていない。なんで??
$_SERVER['HTTPS'] については こちら
'HTTPS'
スクリプトが HTTPS プロトコルを通じて実行されている場合に 空でない値が設定されます。
ちなみにこの現象が発生しているのは お名前.com のレンサバ。
ほかのレンサバや VPS, AWS とかでは $_SERVER['HTTPS'] はきっちり on などを返す。
レンサバ側の大本の設定がなにか悪さをしていそう。
$_SERVER['HTTPS'] が設定されない原因は問い合わせたところ、
「設定されないからこっちで対応して」
といったシンプルな回答だった。
ググったらわかったけど、ロードバランサーを介して https -> http にリバースプロキシしている環境だとあるあるの事象みたい。
対応
どっちでも好きな方で。
A 案. .htaccess などで対応
- .htaccess
# $_SERVER['HTTPS'] が設定されない環境向け
# ロードバランサーを介して https -> http にリバースプロキシしている環境とか
SetEnvIf X-Forwarded-Proto https HTTPS=on
B 案. Laravel プログラム側で対応
- app/Providers/AppServiceProvider.php
public function boot()
{
// レンサバの設定と相性が悪く、https でアクセスしているにも関わらず $_SERVER['HTTPS] が設定されず、
// Laravel, Symfony 側の処理で http 始まりの URL で処理されてしまう。
// config('app.url') が https 始まりか?http 始まりか? をもとに強制的に scheme を設定する
\Illuminate\Support\Facades\URL::forceScheme(\Str::startsWith(config('app.url'), 'https') ? 'https' : 'http');