こんぶにのブログ

エンジニアという職業を通して学んだことを発信するブログです。

【エンジニア3年目でやっと分かった】リダイレクトとURLエンコードの仕組みと必要性

前回

以下の部分まで進んだ。

konbuni.net

リダイレクト

実際にやってみる

今まではあるページに遷移するもの、と認識。
もっと厳密にいうと、
「レスポンスヘッダーのLocationに記されたURLにファイルを取得しに行っている」
ということらしい。

試しにchromeの開発者モードを開き、以下の二つのURLにアクセスしてみる。

https://kmaebashi.com/programmer/webserver

https://kmaebashi.com/programmer/webserver/

/が付いていないほうはリダイレクト(301)が発生。

付いていないほうは発生せず、一発で200 OKが返ってくる。

何故こうなるか・必要か

webserverの後に/をつけないと、Webサーバー側ではディレクトリだと認識されない。
つまり、webserverという名前のファイルをください!という命令だとWebサーバーは解釈する。
しかし、そんなファイルはないのでこのままなにもしなければ、404Not Foundとなってしまう。
そこで、もしwebserverというファイルが無くても、そういう名前のディレクトリ(フォルダ)があった場合は、
自動的にそっちのURLを見てもらうようにする。
すると、利用者は特に何もせずに、勝手にサーバー側でファイルがありそうなディレクトリを見てくれて、
ファイルがあれば返してくれる。
(特にファイル名を指定しないまま末尾/へリダイレクトした場合は、このファイル名のファイルを返すというのを設定できる) つまり、404にすることなく、良い感じに画面を返してくれる仕組みなわけだ。
これが悪用されて、変なサイトに飛ばされたりもするが、私は善の使い方を心がけようと思った。
ちなみに、TopページのURLの場合(例なら、https://kmaebashi.com)、
末尾に/はつけないのが普通。
何故なら、/をつけなかった場合に返却するファイルをWebサーバ側で設定するから。
これがTopページじゃない場合だと、/はつけたほうが良い。
何故なら、/なしだと一回リダイレクトを挟んで、/ありに移動するという二度手間になって、サーバーに負担がかかるから。

URLエンコード

存在意義

URLには基本的にアルファベットを入力する。
しかし、リクエストしたいファイル名が日本語である場合がある。
その場合はURLに日本語を入力しなければいけないが、それが出来ないので、変換しようという話。

確認してみた

Main.phpを起動し、chromeで以下のリクエストを送ってみた。
http://localhost:8001/ファイル名.jpg

実際には以下のような感じで送られていることが分かった。
http://localhost:8001/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D.jpg
一文字ずつ、%区切りで元の文字列を解釈、16進数の対応コードに変換されている。

クエリストリング (query-stirng)

URLの末尾に?を付けて送ることができる文字。
例えばGoogleで「ごはん」としらべてみるとURLはこんな感じになる。
https://www.google.com/search?q=ごはん(略)

クエリストリングの困った点

クエリストリングの部分だけはブラウザによってエンコードの方法が違う。
これだと、挙動がブラウザによって変わってしまうらしい。
対策として、form属性でgetを指定して、accept-charsetを指定する。
そうすると、そのhtmlと同じ文字コードでクエリストリングを送ってくれるらしいので
ブラウザごとの違いが出ずに済む。
今度使ってみよう。

実装してみた

Server.phpはこんな感じ。
これをインスタンス化してserverStartを実行するのみなので、Main.phpは省略。

<?php
// localhostのport8001を使用
class Server
{
    public $address = '127.0.0.1';
    public $port = '8001';
    // DocumentRoot
    const DOCUMENT_ROOT = 'C:\Repo\basic-web-app\redirect-test\document';
    const CONTENT_TYPE_TEXT_HTML = 'text/html';
    const CONTENT_TYPE_IMAGE_JPEG = 'image/jpeg';
    const CONTENT_TYPE_IMAGE_PNG = 'image/png';
    
    const CONTENT_TYPE_MAPPING_LIST = [
        'html' => self::CONTENT_TYPE_TEXT_HTML,
        'htm' => self::CONTENT_TYPE_TEXT_HTML,
        'jpg' => self::CONTENT_TYPE_IMAGE_JPEG,
        'jpeg' => self::CONTENT_TYPE_IMAGE_JPEG,
        'png' => self::CONTENT_TYPE_IMAGE_PNG,
    ];
    
    public function serverStart() : void 
    {
        // ソケットを作成。引数はphpに定義されている関数を使用。公式より。
        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        
        // ソケットに名前を付けます。名前がないとどのアドレスにも関連付けられていないとみなされ、何も出来ません。
        socket_bind($socket, $this->address, $this->port);
        
        // ソケット上で接続待ち(listen)します。クライアントが上記で作ったソケットに接続するのをひたすら待ちます。
        // 5個の処理用キューを用意しておきます。
        socket_listen($socket, 5);
        
        while(true) {
            // もしクライアントからソケットへの接続が来た場合、それを許可します。
            $acceptSocket = socket_accept($socket);

            $msg = "TcpServerに接続できました。\n";
            echo $msg;

            // ソケット接続先のクライアントから書き込まれたデータを読み取ります。
            $out = socket_read($acceptSocket, 2048);

            $fileName = 'text/server_recv.txt';
            $recvFile = fopen($fileName, 'w+');
            fwrite($recvFile, $out);

            // 今は上の書き込んだ文の最後のところにポインタがあるはずだから
            // ファイルポインタを一番前まで戻す。
            rewind($recvFile);

            // 受け取ったリクエストを1行ずつ解釈する
            // どのファイルがリクエストされているかを、httpリクエストの中から見つける
            $requestFileName = '';
            while($line = fgets($recvFile)) {
                if (substr($line, 0, 3) == 'GET') {
                    $back = strripos($line, " ");
                    $front = mb_strpos($line, " ");
                    $length = $back - $front;
                    // それぞれ±2にすることで、ついでに先頭のスラッシュも消す
                    $requestFileName = substr($line, $front + 2, $length - 2);

                    // urlをdecode。日本語のファイル名の場合は16進コードで送られて来ちゃうので、変換してからファイルを探す
                    $requestFileName = urldecode($requestFileName);
                    var_dump($requestFileName);
                    break;
                };
            }

            // 連結したパスの文字列
            $path = self::DOCUMENT_ROOT . "\\" . $requestFileName;

            var_dump('strposの結果' . strpos($requestFileName, '.') != false);
            if (strpos($requestFileName, '.') != false && file_exists($path)) {
                // リクエストに.を含む場合はファイルと判断し、開こうとする
                var_dump('ファイル開きまーす');
                $file = fopen(self::DOCUMENT_ROOT . "\\" . $requestFileName, 'rb');
            }

            // $pathのファイルがあるかどうかを基準に、ステータスラインを作成
            if (isset($file) && $file != false) {
                var_dump('200');
                $responseLine = "HTTP/1.1 200 OK\r\n";
                // 取得したindex.htmlの中身をレスポンスボディとして送信する
                // 全部取得できるからこっちのが楽(どでかいのも全部取得しようとするけど)
                $responseBody = stream_get_contents($file);
                $extension = pathinfo($requestFileName, PATHINFO_EXTENSION);
                $contentTypeMime = self::CONTENT_TYPE_MAPPING_LIST[$extension];
                $httpResponse = $this->createHttpResponse($responseLine, $responseBody ,$contentTypeMime);
            } elseif (file_exists($path)) {
                var_dump('300');
                $responseLine = "HTTP/1.1 301 Moved Permanently\r\n";
                // パターン1:トップページにスラッシュを付けずにリダイレクトした場合 -> 自動で/が付けられて2へ(ディレクトリなし、/なし)
                // パターン2:トップページにスラッシュが付けられてきた場合 -> 末尾にindex.htmlを付与(ディレクトリなし、/あり)
                // パターン3:ディレクトリにスラッシュを付けずにリダイレクトした場合 -> 末尾に/index.htmlを付与(ディレクトリあり、/なし)
                // パターン4:ディレクトリにスラッシュが付けられてきた場合 -> 末尾にindex.htmlを付与(ディレクトリあり、/あり)
                var_dump('stroposの位置' . strpos($requestFileName,  "/"));
                if (strpos($requestFileName,  "/") != false) {
                    var_dump('/あり');
                    $requestFileName =  '/' . $requestFileName;
                    $requestFileName = $this->addIndexHtml($requestFileName);
                } else {
                    var_dump('/なし');
                    $requestFileName =  '/' . $requestFileName . '/';
                    $requestFileName = $this->addIndexHtml($requestFileName);
                }
                $location = "Location: " . 'http://localhost:8001' . $requestFileName;
                $requestFileName = str_replace('//', "/", $location);

                $httpResponse = $this->createRedirectResponse($responseLine, $location);
                var_dump($location);
            } else {
                var_dump('404');
                $responseLine = "HTTP/1.1 404 Not' Found\r\n";
                // リクエストされたファイルがなかったらこっちをレスポンスボディにする
                $responseBody = "
                    <html>
                    <head>
                        <meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">
                    </head>
                    <body>
                        <h1>404 NotFound</h1>
                        <h2>{$requestFileName}が見つかりませんでした。</h2>
                    </body>
                    
                    </html>
                ";
                $contentTypeMime = self::CONTENT_TYPE_MAPPING_LIST['html'];
                $httpResponse = $this->createHttpResponse($responseLine, $responseBody ,$contentTypeMime);
            };
            socket_write($acceptSocket, $httpResponse, strlen($httpResponse));
        }
        
        // 確立された接続を閉じます
        socket_close($acceptSocket);
        
        // ソケットそのものを終了
        socket_close($socket);
    }

    /**
     * ファイルありのhttpレスポンスを返す
     * 200、400番台
     */
    function createHttpResponse($responseLine, $responseBody, $contentTypeMime) : string 
    {
        $headerContentType =  "Content-type: " . $contentTypeMime . "\r\n";

        $responseHeader = '';
        $responseHeader .= "Host: modoki/0.1\r\n";
        $responseHeader .= "Connection: Close\r\n";
        
        $responseHeader .= $headerContentType;
        $responseHeader .= "\r\n";

        $httpResponse = $responseLine . $responseHeader . $responseBody;

        return $httpResponse;
    }

    /**
     * リダイレクト用httpレスポンス作成
     */
    function createRedirectResponse($responseLine, $location) : string 
    {
        $responseHeader = '';
        // $responseHeader += '';// 余裕あれば時間も送信する
        $responseHeader .= "Host: modoki/0.1\r\n";
        $responseHeader .= "Connection: Close\r\n";

        $httpResponse = $responseLine . $responseHeader . $location;

        return $httpResponse;

    }

    /**
     * 送られてきたパスの最後に/を付与して返却
     */
    function addIndexHtml($path) : string 
    {
        return $path . 'index.html';
    }
}

理解最優先で作ったのであまりきれいではない。
あとは欠陥があって、
トップページ/なしから/ありへブラウザが自動リダイレクトしてくれるパターンの時、
localhost:8001//みたいになってしまう。
頑張って直そうと奮闘したのだけど、「Webサーバを理解する」という本質からそれていることに気付きやめた。

並列処理もできてないし、上記の欠陥はあるが、リクエストはしっかり返ってくるのでよし。