11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

なぜここをソースコードリーディングするのか?

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

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

環境のお話

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

調べたきっかけの事象

httpsでアクセスしているのに、Formのactionに設定されるURLがhttpになっていた。

スクリーンショット 2019-02-16 15.07.26.png スクリーンショット 2019-02-16 15.07.49.png

どうしてこうなったのか

「X_FORWARDED_PROTO」を利用したhttps判定が行われていなかったため

なぜ判定が行われなかったか?

App\Http\Middleware\TrustProxies
に信用するロードバランサが指定されていなかったから。

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」から追っていきます。

ヘルパーが定義されているファイルを見てみます。

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')って何者やねん。ということで何者なのか確認します。

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」を返しているようです。
次はUrlGeneratorrouteメソッドを見てみます。

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に進みます

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の内容は↓です。

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作っているところきました。
スキーマとドメイン部分を作っているのは↓の部分です。

vendor/laravel/framework/src/Illuminate/Routing/RouteUrlGenerator.php
$root = $this->replaceRootParameters($route, $domain, $parameters), // "https://localhost" が返ってくる
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のスキーマは以下の処理で決定されています。

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が設定されていなくても、
サーバ変数にHTTPSoff以外が設定されていた場合は「HTTPS接続」とみなされます。

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 = '*'がでてきます。

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

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.phpapp('url')に何が設定されているか確認で真面目にApplication.php見に行ってますが、
アテが無いときとかは ↓ みたいなコード書いてクラス名確認したりします。

dd(app('url'));
スクリーンショット 2019-02-17 14.21.42.png

余談2

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

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」になった変数などを教えてくれたこと。

11
8
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?