生に近い HTTP リクエスト情報を調査するために Ruby で Web サーバを作って Heroku にデプロイ

## 概要
- できるだけ生に近い HTTP リクエスト情報を調査するために Web サーバを作る
- Ruby の標準ライブラリのみを使用する
- Web サーバを macOS Catalina で動作させて curl からの HTTP リクエストを確認する
- Web サーバを Heroku で動作させて curl からの HTTP リクエストを確認する

## Web サーバのソースコード

Ruby 標準ライブラリのみを使用する。
マルチスレッドには非対応。

```myserver.rb
require 'socket'

# 標準出力を同期モードに設定
$stdout.sync = true

# 接続を受け付けるポート番号を決定
# 環境変数 PORT が設定されているならそれを設定
port = 8000
port = ENV['PORT'].to_i if ENV['PORT']

# サーバー接続をオープン
server = TCPServer.open(port)

# HTTP リクエストを待ち続ける
loop do

  begin

    # TCPSocket オブジェクトを取得
    socket = server.accept

    # 受け付けた日時を出力
    puts "[info]#{Time.new}"

    # HTTP リクエスト開始行を出力
    if not req_start_line = socket.gets
      puts '[info]req_start_line is nil'
      next
    end
    puts "#{req_start_line}"

    # HTTP リクエストヘッダーを1行ずつ出力
    while req_header = socket.gets.chomp
      puts "#{req_header}"
      break if req_header == '' # ヘッダー終了
      # Content-Length ヘッダーがあれば値を変数にセット
      h = req_header.split(':')
      content_length = h[1].strip.to_i if h[0].strip.downcase == 'content-length'
    end

    # Content-Length がある場合はボディを出力
    if content_length != nil
      puts socket.read(content_length)
    end

    # HTTP レスポンスを返す
    # 本文データ
    body = "<html><body>Hello, world</body></html>\r\n"
    # ステータス行
    socket.write "HTTP/1.1 200 OK\r\n"
    # ヘッダー
    socket.write "Server: #{RUBY_DESCRIPTION}\r\n"
    socket.write "Content-Type: text/html; charset=utf-8\r\n"
    socket.write "Content-Length: #{body.bytesize}\r\n"
    socket.write "Connection: close\r\n"
    # 空行
    socket.write "\r\n"
    # 本文
    socket.write body

  rescue => e
    puts e.full_message

  ensure
    # HTTP 接続を閉じる
    puts '[info]close this socket'
    socket.close
  end

end

server.close
```

## macOS Catalina で動かす

macOS Catalina + Ruby 2.7.0 の環境で Web サーバプログラム myserver.rb を起動する。

```
$ ruby myserver.rb
```

### curl から HTTP GET リクエストする例

クライアント側のコマンドと出力結果。

```
$ curl -i -H 'small: sss' -H 'LARGE: LLL' http://localhost:8000/foo/?aaa=xxx
HTTP/1.1 200 OK
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Connection: close

<html><body>Hello, world</body></html>
```

サーバ側の出力結果。

```
[info]2020-02-04 08:03:08 +0900
GET /foo/?aaa=xxx HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.68.0
Accept: */*
small: sss
LARGE: LLL

[info]close this socket
```

### curl から HTTP POST リクエストする例

クライアント側のコマンドと出力結果。

```
$ curl -i -H 'Content-Type: application/json' http://localhost:8000/bar/ -d '{"foo": {"bar": ["あいうえお"]}}'
HTTP/1.1 200 OK
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Connection: close

<html><body>Hello, world</body></html>
```

サーバ側の出力結果。

```
[info]2020-02-04 08:03:14 +0900
POST /bar/ HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.68.0
Accept: */*
Content-Type: application/json
Content-Length: 37

{"foo": {"bar": ["あいうえお"]}}
[info]close this socket
```

## Heroku で動かす

Heroku + Ruby 2.7.0 の環境で Web サーバを起動する。

できるだけ生に近い HTTP リクエストを調査したいが、Heroku のリバースプロキシが間に入るためリクエストヘッダがいくつか増えてしまう問題がある(許容するしかない)。

### Heroku にデプロイするために必要なファイル

サーバプログラム myserver.rb 以外に Gemfile, Gemfile.lock, Procfile が必要。

#### Gemfile

```Gemfile
ruby '2.7.0'
```

#### Gemfile.lock

```Gemfile.lock
GEM
  specs:

PLATFORMS
  ruby

DEPENDENCIES

RUBY VERSION
   ruby 2.7.0p0

BUNDLED WITH
   2.1.2
```

#### Procfile

```Procfile
web: ruby myserver.rb
```

### curl から HTTP GET リクエストする例

クライアント側のコマンドと出力結果。

```
$ curl -i -H 'small: sss' -H 'LARGE: LLL' https://example.herokuapp.com/foo/?aaa=xxx
HTTP/1.1 200 OK
Date: Mon, 03 Feb 2020 23:03:28 GMT
Connection: keep-alive
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Via: 1.1 vegur

<html><body>Hello, world</body></html>
```

サーバ側の出力結果。

```
[info]2020-02-03 23:03:28 +0000
GET /foo/?aaa=xxx HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: curl/7.68.0
Accept: */*
Small: sss
Large: LLL
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 1
X-Request-Start: 1580771008907
Total-Route-Time: 0

[info]close this socket
```

### curl から HTTP POST リクエストする例

クライアント側のコマンドと出力結果。

```
$ curl -i -H 'Content-Type: application/json' https://example.herokuapp.com/bar/ -d '{"foo": {"bar": ["あいうえお"]}}'
HTTP/1.1 200 OK
Date: Mon, 03 Feb 2020 23:03:36 GMT
Connection: keep-alive
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Via: 1.1 vegur

<html><body>Hello, world</body></html>
```

サーバ側の出力結果。

```
[info]2020-02-03 23:03:37 +0000
POST /bar/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: curl/7.68.0
Accept: */*
Content-Type: application/json
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 1
X-Request-Start: 1580771017243
Total-Route-Time: 0
Content-Length: 37

{"foo": {"bar": ["あいうえお"]}}
[info]close this socket
```

## 参考資料

- [library socket \(Ruby 2\.7\.0 リファレンスマニュアル\)](https://docs.ruby-lang.org/ja/2.7.0/library/socket.html)
- [HTTP のメッセージ \- HTTP \| MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Messages)
- [HTTP/1\.x のコネクション管理 \- HTTP \| MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Connection_management_in_HTTP_1.x)
- [Getting Started on Heroku with Ruby \| Heroku Dev Center](https://devcenter.heroku.com/articles/getting-started-with-ruby?singlepage=true)
- [Heroku Ruby Support \| Heroku Dev Center](https://devcenter.heroku.com/articles/ruby-support)
- [The Procfile \| Heroku Dev Center](https://devcenter.heroku.com/articles/procfile)