be-kan's blog

今しかできない勉強をする。

RubyでHTTPサーバを立てる②

昨日の投稿(https://be-kan.hatenablog.jp/entry/2018/05/24/012445)の続きになります。

元記事はこちら
blog.appsignal.com


内容的には
・HTTPサーバの上でRails Appを動かす
のみです。

当内容、GitHubソースコードをあげています。
https://github.com/be-kan/http_server_in_ruby


HTTPサーバの上でRaila Appを動かす

使用するRails Appはこちらになります。
https://github.com/jeffkreeftmeijer/wups.git

ただデータをpostしその一覧を取得できるだけのものです。
もちろん自分で用意していただいても構いません。

http_server.rbがあるディレクトリで、Rails Appをgit submoduleで管理します。

$ ls
http_server.rb

$ git submodule add https://github.com/jeffkreeftmeijer/wups.git sample_blog

前回、http_server.rbは以下のコードで終了していると思います。

require 'socket'
require 'rack'
require 'rack/lobster'

app = Rack::Lobster.new
server = TCPServer.new 5678

while session = server.accept
  request = session.gets
  puts request

  method, full_path = request.split(' ')
  path, query = full_path.split('?')

  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query
  })

  session.print "HTTP/1.1 #{status}\r\n"

  headers.each do |key, value|
    session.print "#{key}: #{value}\r\n"
  end

  session.print "\r\n"

  body.each do |part|
    session.print part
  end

  session.close
end

Rack::Lobster Appを動かしていましたが、これをRails Appに変更します。

require 'socket'
require_relative 'sample_blog/config/environment'

app = Rails.application
server = TCPServer.new 5678

これでサーバを立ててアクセスしてみると、以下のようなエラーがきます。

500 Internal Server Error
If you are the administrator of this website, then please read this web application's log file and/or the web server's log file to find out what went wrong.

言われた通りにlogを確認すると、

Error during failsafe response: Missing rack.input

とのことです。
Rack Appを動かすには、REQUEST_METHOD, PATH_INFO, QUERY_STRINGだけで十分だったのですが、Rails Appを動かすにはそれだけでは不十分のようです。

Rackには、Rack::Lintという、RackがRailsに渡すべき変数を用意してくれるものがあるので、
Rails.applicationをRack::Lintでラッピングします。

require 'socket'
require_relative 'sample_blog/config/environment'

app = Rack::Lint.new(Rails.application)
server = TCPServer.new 5678

これでもう一度サーバを立ててlocalhost:5678にアクセスすると、サーバがクラッシュしました。

~path/to/rack/lint.rb:20:in `assert': env missing required key SERVER_NAME (Rack::Lint::LintError)

Rack::LintはRailsに十分に変数を渡しますが、そもそもRack::Lintに必要な変数を渡せていないようです。
エラーで表示された変数を追加して行くと、最終的に以下のようになります。

  # ...
  method, full_path = request.split(' ')
  path, query = full_path.split('?')

  input = StringIO.new
  input.set_encoding 'ASCII-8BIT'

  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query || '',
    'SERVER_NAME' => 'localhost',
    'SERVER_PORT' => '5678',
    'rack.version' => [1,3],
    'rack.input' => input,
    'rack.errors' => $stderr,
    'rack.multithread' => false,
    'rack.multiprocess' => false,
    'rack.run_once' => false,
    'rack.url_scheme' => 'http'
  })

すると、以下のようなメッセージがブラウザに表示されます。
f:id:kambe3141:20180524204811p:plain
エラーとはいえ、ついにRails側のエラーが来ましたね!

ログを確認すると、

IPAddr::InvalidAddressError: invalid address

というエラーです。
クライアントのIPアドレスを指定しなければなりません。

今はlocalhostで動かしているので、以下のように設定します。

status, headers, body = app.call({
      # ...
      'SERVER_PORT' => '5678',
      'REMOTE_ADDR' => '127.0.0.1',
      'rack.version' => [1,3],
      # ...
  })

f:id:kambe3141:20180524205220p:plain

ついにRailsが表示されました。

ここから、Rails Appに対して、POSTを行います。
Rails App的にはすでにpostに対応しているので、あとはサーバとのつなぎこみのみです。

ちなみに、元記事にはありませんが、Rails Appのデータベースがないので、

rake db:migrate

でデータベースを作成しておきましょう。

サーバを立て、http://localhost:5678/postsにアクセスし、New Postから新規にpostを作成しようとすると

ActionController::InvalidAuthenticityToken

と言われます。
通常ならauthenticity tokenはリクエストと共に送信されるのですが、今のコードでは何もポストデータを送っていないようです。

そこで、session.getsで取得したリクエストの情報を、headerにハッシュとして受け取ります。
リクエストの大きさ(何バイトか)をあらかじめ知るためにbodyにContent-Lengthの情報を入れ、さらにrack.inputには先のbodyで初期化したString.IOインスタンスを入れます。
headerのクッキーも指定し、authenticityをクリアします。

  headers = {}
  while (line = session.gets) != "\r\n"
    key, value = line.split(':', 2)
    headers[key] = value.strip
  end

  body = session.read(headers["Content-Length"].to_i)
  
  # ...

  status, headers, body = app.call({
    # ...
    'REMOTE_ADDR' => '127.0.0.1',
    'HTTP_COOKIE' => headers['Cookie'],
    'rack.version' => [1,3],
    'rack.input' => StringIO.new(body),
    'rack.errors' => $stderr,
    # ...
  })

これで無事POSTもできるようになりました!