ここまでで、CSSや画像を含むページを表示出来るようになりました。 今回も
の書籍を参考に、Rubyで
- URLエンコードの対応をする
- ファイルが存在しない時に404 NOT FOUNDを返す
- ディレクトリトラバーサル脆弱性に対応する
- ドメインだけ/ディレクトリだけ指定されたときに対応する
を実装して、静的なサーバを完成させたいと思います。
目次
- 目次
- 動作環境
- 実装
- ソースコード
動作環境
実装
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も処理できるようになりました。
参考サイト
- module URI (Ruby 2.7.0 リファレンスマニュアル)
- class CGI - RDoc Documentation
- URI.escape is obsolete. Percent-encoding your query string
- RubyのCGI.escapeとURI.encodeについて | 酒と涙とRubyとRailsと
ファイルが存在しない時に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
が/
で終わっている時には@path
にindex.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
と、末尾に'/'無しでたたいてみると、
- ステータスコード301が返る
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