なぜここをソースコードリーディングするのか?
AWS ELB配下にLaravelをdeployしたとき、生成されるURLがhttpsになってくれなくて困った
どうすれば解決できるのかはすぐわかったけど、どういう処理が行われているのかが気になったので、
日曜大工的感覚でソースコードリーディングしていきます。
特にすごいことは書いてないのであしからず。。。(自分の勉強メモ的な感覚です)
環境のお話
Laravel 5.7
Nginx 1.15.8
PHP 7.2.12
※ docker環境で動かしています
調べたきっかけの事象
httpsでアクセスしているのに、Formのactionに設定されるURLがhttpになっていた。
どうしてこうなったのか
「X_FORWARDED_PROTO」を利用したhttps判定が行われていなかったため
↓
なぜ判定が行われなかったか?
↓
App\Http\Middleware\TrustProxies
に信用するロードバランサが指定されていなかったから。
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array
*/
// protected $proxies; //←ココを
//↓
protected $proxies = '*'; //←こうする
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
}
どういう処理をしているのか?
$proxies
に*
を入れたらOKというのはわかったけど、どういう処理をしているんだろう?
ということでLaravelのソースを追ってみます。
ヘルパーの「route」から追っていきます。
ヘルパーが定義されているファイルを見てみます。
// 略
/**
* Generate the URL to a named route.
*
* @param array|string $name
* @param mixed $parameters
* @param bool $absolute
* @return string
*/
function route($name, $parameters = [], $absolute = true)
{
return app('url')->route($name, $parameters, $absolute);
}
// 略
app('url')
って何者やねん。ということで何者なのか確認します。
public function registerCoreContainerAliases()
{
foreach ([
// 略
'url' => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class], //☆
// 略
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
ありました。 app('url')
は「\Illuminate\Contracts\Routing\UrlGenerator」を返しているようです。
次はUrlGenerator
のroute
メソッドを見てみます。
public function route($name, $parameters = [], $absolute = true)
{
if (! is_null($route = $this->routes->getByName($name))) {
return $this->toRoute($route, $parameters, $absolute);
}
throw new InvalidArgumentException("Route [{$name}] not defined.");
}
呼んでいるtoRoute
に進みます
protected function toRoute($route, $parameters, $absolute)
{
return $this->routeUrl()->to(
$route, $this->formatParameters($parameters), $absolute
);
}
protected function routeUrl()
{
if (! $this->routeGenerator) {
$this->routeGenerator = new RouteUrlGenerator($this, $this->request);
}
return $this->routeGenerator;
}
RouteUrlGenerator
インスタンスを生成して、to
を実行していますね。
to
の内容は↓です。
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('/\{.*?\}/', $uri)) {
throw UrlGenerationException::forMissingParameters($route);
}
// 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;
}
ついにURL作っているところきました。
スキーマとドメイン部分を作っているのは↓の部分です。
$root = $this->replaceRootParameters($route, $domain, $parameters), // "https://localhost" が返ってくる
protected function replaceRootParameters($route, $domain, &$parameters)
{
$scheme = $this->getRouteScheme($route);//☆ココ
return $this->replaceRouteParameters(
$this->url->formatRoot($scheme, $domain), $parameters
);
}
protected function getRouteScheme($route)
{
if ($route->httpOnly()) {
return 'http://';
} elseif ($route->httpsOnly()) {
return 'https://';
}
return $this->url->formatScheme();//☆ココ
}
public function formatScheme($secure = null)
{
if (! is_null($secure)) {
return $secure ? 'https://' : 'http://';
}
if (is_null($this->cachedSchema)) {
$this->cachedSchema = $this->forceScheme ?: $this->request->getScheme().'://'; //☆ココ
}
return $this->cachedSchema;
}
応答するURLのスキーマは以下の処理で決定されています。
public function getScheme()
{
return $this->isSecure() ? 'https' : 'http';
}
public function isSecure()
{
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);
}
// 信用されている接続元(リモートアドレス)かどうかをチェックしている。
public function isFromTrustedProxy()
{
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
}
HTTPヘッダーを使ってスキーマの判定を行うのですが、まず接続元が信用されいるかのチェックを行います。
信用されている接続元だった場合、HTTPリクエストヘッダからX_FORWARDED_PROTO
を取得します。
取得したヘッダーの内容が「'https', 'on', 'ssl', '1'」のいずれかであれば、「HTTPS接続」とみなします。
ちなみに、信用されたプロキシじゃなくても AND X_FORWARDED_PROTO
が設定されていなくても、
サーバ変数にHTTPS
にoff
以外が設定されていた場合は「HTTPS接続」とみなされます。
location ~ \.php$ {
fastcgi_param HTTPS on; #←ココ
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
信用された接続元のIPアドレスはどこで設定している?
ここで最初に設定した$proxies = '*'
がでてきます。
以下の処理で信用された接続元を設定しています。
protected function setTrustedProxyIpAddresses(Request $request)
{
$trustedIps = $this->proxies ?: $this->config->get('trustedproxy.proxies');
// Trust any IP address that calls us
// `**` for backwards compatibility, but is deprecated
if ($trustedIps === '*' || $trustedIps === '**') { // ☆ココ!!
return $this->setTrustedProxyIpAddressesToTheCallingIp($request);
}
// Support IPs addresses separated by comma
$trustedIps = is_string($trustedIps) ? array_map('trim', explode(',', $trustedIps)) : $trustedIps;
// Only trust specific IP addresses
if (is_array($trustedIps)) {
return $this->setTrustedProxyIpAddressesToSpecificIps($request, $trustedIps);
}
}
private function setTrustedProxyIpAddressesToTheCallingIp(Request $request)
{
$request->setTrustedProxies([$request->server->get('REMOTE_ADDR')], $this->getTrustedHeaderNames());
}
*
または**
が設定されているときはsetTrustedProxyIpAddressesToTheCallingIp
が呼ばれて、
呼び出し元IPアドレスを信用するという処理になっていました!
なので*
または**
を設定すると問答無用で信用された接続元として処理されるわけですね。
余談1
helpers.php
でapp('url')
に何が設定されているか確認で真面目にApplication.php
見に行ってますが、
アテが無いときとかは ↓ みたいなコード書いてクラス名確認したりします。
dd(app('url'));
余談2
※ $cachedSchema
がdeprecatedになってました。 Laravel 5.8での変更を予定しているらしいです。
/**
* A cached copy of the URL scheme for the current request.
*
* @deprecated In 5.8, this will change to $cachedScheme
* @var string|null
*/
protected $cachedSchema;
最後に
最近PhpStorm導入したのですが、ソースコードリーディングがはかどりますね。 定義先にクリックするだけで飛んでくれたり。
あと地味に嬉しかったのは「deprecated」になった変数などを教えてくれたこと。