Laravelコードリーディング  LB配下環境でのrouteヘルパーの動作

# なぜここをソースコードリーディングするのか？
AWS ELB配下にLaravelをdeployしたとき、生成されるURLがhttpsになってくれなくて困った
どうすれば解決できるのかはすぐわかったけど、どういう処理が行われているのかが気になったので、
日曜大工的感覚でソースコードリーディングしていきます。

特にすごいことは書いてないのであしからず。。。(自分の勉強メモ的な感覚です)

# 環境のお話
Laravel 5.7
Nginx 1.15.8
PHP 7.2.12
※ docker環境で動かしています

# 調べたきっかけの事象
httpsでアクセスしているのに、Formのactionに設定されるURLがhttpになっていた。

<img width="1397" alt="スクリーンショット 2019-02-16 15.07.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/108431/87481d8f-5731-5a85-9cd0-11b246b32f55.png">
<img width="730" alt="スクリーンショット 2019-02-16 15.07.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/108431/9873b52c-f458-66ec-c6d7-68d8f10df15e.png">

# どうしてこうなったのか
「X_FORWARDED_PROTO」を利用したhttps判定が行われていなかったため
↓
なぜ判定が行われなかったか？
↓
App\Http\Middleware\TrustProxies
に信用するロードバランサが指定されていなかったから。

```php:App\Http\Middleware\TrustProxies.php
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」から追っていきます。
ヘルパーが定義されているファイルを見てみます。

```php:vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
// 略
    /**
     * 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')`って何者やねん。ということで何者なのか確認します。

```php:vendor/laravel/framework/src/Illuminate/Foundation/Application.php
    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`メソッドを見てみます。

```php: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 InvalidArgumentException("Route [{$name}] not defined.");
    }
```

呼んでいる`toRoute`に進みます

```php:vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
    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`の内容は↓です。

```php: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('/\{.*?\}/', $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作っているところきました。
スキーマとドメイン部分を作っているのは↓の部分です。

```php:vendor/laravel/framework/src/Illuminate/Routing/RouteUrlGenerator.php
$root = $this->replaceRootParameters($route, $domain, $parameters), // "https://localhost" が返ってくる
```

```php:vendor/laravel/framework/src/Illuminate/Routing/RouteUrlGenerator.php
    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のスキーマは以下の処理で決定されています。

```php:vendor/symfony/http-foundation/Request.php
    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接続」とみなされます。

```nginx:nginx.conf
    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 = '*'`がでてきます。

以下の処理で信用された接続元を設定しています。

```php:src/vendor/fideloper/proxy/src/TrustProxies.php
    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`見に行ってますが、
アテが無いときとかは ↓ みたいなコード書いてクラス名確認したりします。

```php
dd(app('url'));
```

<img width="446" alt="スクリーンショット 2019-02-17 14.21.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/108431/d068f948-3152-2c2d-652d-999db1602c57.png">



# 余談2
※ `$cachedSchema` がdeprecatedになってました。 Laravel 5.8での変更を予定しているらしいです。

```php:vendor/laravel/framework/src/Illuminate/Routing/RouteUrlGenerator.php
    /**
     * 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」になった変数などを教えてくれたこと。