RubyでWebサーバを作ってみた2:静的なサーバを完成させる

ここまでで、CSSや画像を含むページを表示出来るようになりました。 今回も

gihyo.jp

の書籍を参考に、Ruby

を実装して、静的なサーバを完成させたいと思います。

目次

動作環境

実装

URLエンコードの対応をする

URLエンコードは、URLに含まれるスペース、記号、日本語文字などの特殊な文字を安全に表現するための方法です。 これらの文字をURLエンコードすることで、安全にURLに組み込むことができます。 サーバーサイドでは、URLエンコードされたパスをデコードして、正しいファイルパスを取得します。

URI.decodeは非推奨

URI.escape is obsolete. Percent-encoding your query stringによると、 URI.encodeは、文字列全体を単純にgsubで置換しているだけで、RFC-3896の仕様に準拠しているわけではないようで、URI.decodeも非推奨になっています。

今回は、CGI仕様の、CGI.unescapeを使おうと思います。
(RFC 3986, RFC 3987, and RFC 6570に準拠しているaddressableもあります。)

CGI.unescapeでdecodeする

  def recv
    request = @client.gets
    @path = request.split[1] if request.start_with?('GET')
    request = @client.gets until request.chomp.empty?
     # 追加
    @path = CGI.unescape(@path)
  end

日本語\index.htmlを用意する

C:\mywebsite\フォルダに「日本語」というフォルダを作成して、その中にindex.htmlを用意しました。

// \mywebsite\日本語\index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body {
            background-color: #66ccff;
            text-align: center;
            padding: 20px;
        }
        img {
            width: 80px;
            height: auto;
        }
        a {
            font-size: 50%;
        }
    </style>
</head>
<body>
    日本語 works💐<br>
</body>
</html>

確認

  • ブラウザでhttp://localhost:8001/日本語/index.htmlをたたいてみます
    非Ascii文字の入ったURLも処理できるようになりました。

参考サイト

ファイルが存在しない時に404 NOT FOUNDを返す

存在しないファイルをリクエストされた場合の対応

@pathで指定されたファイルが存在した時はステータスコード200を、ファイルが存在しなかったときはステータスコード404を返すようにします

def send_response
    if File.exist?("#{DOCUMENT_ROOT}#{@path}")
      generate_response('200 OK', "#{DOCUMENT_ROOT}#{@path}")
    else
      generate_response('404 Not Found', "#{DOCUMENT_ROOT}/404.html")
    end
  end

  def generate_response(status_code, file_path)
  # レスポンスの中身
  end

404.htmlを用意する

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Not Found</title>
    <style>
        body {
            text-align: center;
            font-size:xx-large;
            padding: 20px;
        }
        
    </style>
</head>
<body>
    <h1>404<br>Not Found</h1></body>
</html>

確認

存在しないURLhttp://localhost:8001/foo.htmlを叩いて、404が返ってくることを確認します。
404が返ってきました

ディレクトリトラバーサル脆弱性に対応する

ディレクトリトラバーサルとは、相対パス表記を用いることで、本来アクセスできるはずのないディレクトリやファイルにアクセス出来てしまうことです。 この脆弱性から、機密情報が漏洩したり、システムの不正な操作が可能になったりする可能性があります。

今回は、上記のコードで、file_pathにドキュメントルートからの絶対パスを指定しているので、相対パスによるディレクトリトラバーサル攻撃は遮断されるはずです。
試しに、

  • http://localhost:8001/../
  • http://localhost:8001/../foo/

等をたたいても、ディレクトリトラバーサルはできません。この対応もできました。

ドメインだけ/ディレクトリだけ指定されたときに対応する

ディレクトリまで指定されたときは、デフォルトでindex.htmlを返す

def send_response
    @path += 'index.html' if @path.end_with?('/')

   (略)
 end

@path/で終わっている時には@pathindex.htmlを加えるようにします。
これで、ルートパスだけ指定されたときでも、トップページを表示できます

users\index.htmlを用意する

C:\mywebsite\フォルダに「users」というフォルダを作成して、その中にindex.htmlを用意しました。

// \mywebsite\users\index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Users</title>
    <style>
        body {
            background-color: #fffce6;
            text-align: center;
            padding: 20px;
        }
        img {
            width: 80px;
            height: auto;
        }
        a {
            font-size: 50%;
        }
    </style>
</head>
<body>
    cat<br>
</body>
</html>

確認

  • http://localhost:8001/users/index.html
  • http://localhost:8001/users/

を叩くと、同じページが表示されることが確認できました。

ディレクトリの後に/を付けずに指定されたときはリダイレクトする

次は、http://localhost:8001/usersと指定されたときです。

仕様の確認

試しにhttps://ja.wikipedia.org/wikiと、末尾に'/'無しでたたいてみると、

ということが分かります

仕様書の確認

仕様書も確認しておきましょう。 RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

The 301 (Moved Permanently) status code indicates that the target resource has been assigned a new permanent URI and any future references to this resource ought to use one of the enclosed URIs.
(略)
The server SHOULD generate a Location header field in the response containing a preferred URI reference for the new permanent URI. The user agent MAY use the Location field value for automatic redirection. The server's response payload usually contains a short hypertext note with a hyperlink to the new URI(s).

ステータスコード301の役割は、クライアントに対して要求されたリソースが恒久的に新しいURLに移動したことを通知することと、レスポンスに新しいURLを示すことであり、新しいURLに対してリダイレクトを行うかどうかは、クライアントの判断次第ということのようです。

実装

def send_response
    @path += 'index.html' if @path.end_with?('/')

    if Dir.exist?("#{DOCUMENT_ROOT}#{@path}/")
      @path += '/index.html'
      generate_response('301 Moved Permanently', "#{DOCUMENT_ROOT}#{@path}")
    elsif File.exist?("#{DOCUMENT_ROOT}#{@path}")
      generate_response('200 OK', "#{DOCUMENT_ROOT}#{@path}")
    else
      generate_response('404 Not Found', "#{DOCUMENT_ROOT}/404.html")
    end
  end

Dir.exist?("#{DOCUMENT_ROOT}#{@path}/")で リクエストされたパスが存在するディレクトリであれば、そのパスの末尾にindex.htmlを追加し、 generate_response@pathを渡します。

  def generate_response(status_code, file_path)
    response = "HTTP/1.0 #{status_code}\r\n"
    response += "Date: #{Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT')}\r\n"
    response += "Server: Modoki/0.1\r\n"

    if status_code == '301 Moved Permanently'
      response += "Location: http://#{HOST_NAME}:#{PORT_NUMBER}#{@path}\r\n"
      response += "\r\n"
    else

      response += "Content-Type: #{content_type}\r\n"
      response += "\r\n"

      content = read_file(file_path)
      response += content
    end
    @client.puts(response)
  end

ステータスコードが 301 Moved Permanently のとき、Location ヘッダフィールドをレスポンスに追加します。

確認

http://localhost:8001/usersをたたいてみます リダイレクトが出来ていることがわかります これで静的な最低限のサーバ機能が揃ったと思います。
次はWebアプリケーションを扱えるWebサーバを作ってみます

ソースコード

https://github.com/kei-kmj/HenacatRuby/commit/072cbc63f326df91a181f57cbff8e9d9f8e45f40