netcat スクリプトで curl からのリクエストを受け取りファイルに保持してステータスコードを返す


# netcat スクリプトで curl からのリクエストを受け取りファイルに保持してステータスコードを返す


## はじめに
ほとんどは Leandro Proença 氏による "Building a Web server in Bash" (https://dev.to/leandronsp/building-a-web-server-in-bash-part-i-sockets-2n8b 以下の一連) を読めば分かることですが、日本語などマルチバイト文字を使うと上手く行かないので、そのことの補足も含めて書きます。また、氏のこの一連の記事は HTTP を理解するのにとても良いものだと思うので、ご一読を勧めます。


## 単に curl によるリクエストを netcat で受け取り表示させる
これは簡単で、ターミナルを2つ開き、片方で
```
netcat-lN 適当なポート番号
```
もう片方で
```
curl -POST localhost:上で待っている適当なポート番号 -d hello,world
```
とすれば、netcat 側に
```
POST / HTTP/1.1
Host: localhost:適当なポート番号
User-Agent: curl/バージョン
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded

hello,world
```
と標準出力されているはずです。
ただし、curl 側がステータスコードを受け取っていないのでそのまま切れず、従って netcat 側もそのまま「hello,world」のすぐ右でカーソルが点滅したまま止まっていると思います。どちらかを Ctrl+c で止めてください。
curl 側にステータスコードを送って切らせるには、例えば取り敢えず "HTTP/1.1 202 Accepted" 辺りを返して切らせるならば、 netcat 側を
```
printf "HTTP/1.1 202 Accepted\r\n\r\n" | netcat-lN 適当なポート番号
```
と変えれば良いです。ここへ向けて前と同様に
```
curl -POST localhost:上で待っている適当なポート番号 -d hello,world
```
とすれば curl 側に
```
HTTP/1.1 202 Accepted

```
と表示されて切れてくれますし、netcat 側も終わります。
しかしこのままでは、curl のリクエストに応じてステータスコードを "202 Accepted" や "204 No content" と変えることが出来ません。

## 取り敢えずリクエスト内容をファイルにする
これも簡単で、netcat 側を
```
netcat -lN 適当なポート番号 > 4_verification
```
にすれば、curl が送ってくるリクエストを 4_verification という名前のファイルに保存することができます。ただしステータスコードを curl 側に送っていないのでやはり両方とも終了できません。

## リクエスト内容をファイルにする 2
なので 4_verification ファイルを検証してそれに応じたステータスコードを送り返したいところです。
しかしここで難問が立ち塞がります。
HTTP リクエストは「そこで終了」という印がありません。netcat で curl の hello,world ポストを受け取ったときカーソルが hello,world のすぐ右で点滅していたのは、そこに改行など終了を示すものが無いからです。つまり
- curl 側が切れてくれないと netcat 側でリクエストが終わったと認識できず
- curl 側が切れるには netcat 側からのステータスコードを受け取る必要があり
- netcat 側がどのステータスコードを送るかは curl からのリクエストを全て受け取ってからしか判断できず
- そのためには curl 側から切れてくれないとリクエストが全て送られたと分からず……

という「缶詰の中の缶切り」のようなことになります。
これを解決するためにヘッダの Content-Length タグを使います。上述の例では
```bash
Content-Length: 11
```
とありますが、これは「hello,world」が11バイトということです。
なのでリクエストのボディ（リクエストの空行より下）を Content-Length タグで示されたバイト数だけ受け取って、そしてそれで終りとみなし、その内容によってステータスコードを curl 側へ送るスクリプトへ netcat から流すようにします。

```
curl -->-> netcat -->-> シェルスクリプト
　↑　　　　　　　　　　　　　↓
   <-<------ステータスコード---
```

こんなイメージです。
まずはリクエストのヘッダとボディをファイルにします。
```bash:netcat_2_file.sh
#!/bin/bash

file_name=$(date "+%s") # 重複しないように unixtime でHTTPリクエスト用ファイルを作る。

while read -r line; do

	trline=$(echo $line | tr -d "\r\n") # 行末から \r\n を取り除く
	echo "$trline" # それを file_name ファイルへ書き込めるよう echo

	content_reg='Content-Length:\s(.*?)'
	[[ "$trline" =~ $content_reg ]] && # 正規表現 'Content-Length:\s(.*?)' と '=~' 演算子を用い、'Content-lengtn: 数字' 形式の行であるかどうかを判定し
	CONTENT_LENGTH=$(echo $trline | sed -E "s/$content_reg/\1/") # その行ならば数字だけを取り出して CONTENT_LENGTH 変数に入れる。

	[ -z "$trline" ] && break # ヘッダとボディの間の空行でこのループを出る。

done > $file_name # 空行までを file_name ファイルに書き込む。

if [[ $CONTENT_LENGTH -gt 0 ]]; then # Content-Length があったならば
	read -n $CONTENT_LENGTH -r line # read の -n オプションで $CONTENT_LENGTH 文字数だけ読み込む
	body=$line # それを body 変数へ入れ
	echo -n $line >> $file_name # さらに file_name ファイルに追加書き込み。
fi
```

## リクエストのボディの有無によってステータスコードを変えて返す
上までの結果だけなら netcat へ流れてくるものを 4_verification ファイルにそのまま流し込んでいるのと変わりませんが、こういった方法だとリクエストの各行ごとに取得し、処理できるようになります。
この例では Content-length タグの行とボディだけを取得して、ボディがあるかないか（大まかに言えば POST か GET か）でステータスコードを "202 Accepted" か "204 No Content" かのどちらかを curl 側に送るようにします。

```
        <-<----202 Accepted-----<----
       ↓                           ↑
cat response -->-> netcat -->-> シェルスクリプト
       ↑                           ↓
        <-<----204 Not Content---<---
```

このようなイメージです。response から netcat にステータスコードを流し、netcat が curl へステータスコードを渡します。
上述のコードに以下を加えます。
```
#if [ -n "$body" ]; then # ボディがあるならば
	printf "HTTP/1.1 202 Accepted\r\n\r\n" > response # 202を返し
else
	printf "HTTP/1.1 204 No Content\r\n\r\n" > response # でなければ 204 を返す。
fi
```
ただしここで response を普通のテキストファイルにすると最初に netacat を介して curl 側に送られるのは（初期状態で response が空ファイルの場合）何も無く、次に送られるのは最初の処理の結果……と、一つずつズレてしまいます。
なので response をパイプファイルにして、そこに何か流し込まれるまで cat で開かれないようにします。
```
mkfifo response
```
これで
```
cat response | netcat-lN 適当なポート番号 | bash netcat_2_file.sh
```
とし、別のターミナルで
```
curl -i -POST localhost:適当なポート番号 -d hello,world
```
と送ると
```
HTTP/1.1 202 Accepted

```
と表示され curl が終了し、netcat 側も終了しています。また、unixtime 名で作られたファイル（24-01-03 あたりでは 170430xxxx くらい）を開いてみると
```
POST / HTTP/1.1
Host: localhost:適当なポート番号
User-Agent: curl/バージョン
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded

hello,world
```
とリクエスト内容がファイルとして保持されています。
この netcat をサーバとしてリクエストを処理させ続けるのならば、
```
while :; do cat response | netcat-lN 適当なポート番号 | bash netcat_2_file.sh ; done
```
と while で回せばいいでしょう。（netcat のオプションに N を加えた方がいいかもしれません）

## ここからが本題
ここまでならば上述した Leandro Proença 氏による記事を読んで書いて試してみれば分かることですが、このままでは欠陥があります。リクエストのボディに、日本語をはじめマルチバイト文字を使うと netcat 側が curl へステータスコードを送れず、両者とも終了しません。
```
cat response | netcat-lN 適当なポート番号 | bash netcat_2_file.sh
```
```
curl -POST localhost:上で待っている適当なポート番号 -d はろわ
```
これは Content-Length タグで示されている数字はバイト数なのに対して、"read -n 数字" で read コマンドが拾えるのは文字数分だからです。上の例では "Content-Length: 9" なので、$CONTENT-LENGT 変数に入るのも 9 です。なので read -n 9 で read は9文字の入力を待っているのに"はろわ"と3文字しか入らず、read コマンドはあと6文字入力されるのを待つわけです。
なので netcat_2_file.sh の
```
read -n $CONTENT_LENGTH -r line
```
の行をバイト数で文字を拾えるものに変えます。
```
line=$(head -c $CONTENT_LENGTH -)
```
head コマンドの -c オプションで、送られる文字列の最初の CONTENT_LENGTH バイト数だけを拾います。" - " をその後に付けることで、head コマンドへ送られるものを標準入力にします。
（参考に
```
head -
```
とするとただのエコーになります）
これで、curl で送られてくるリクエストをファイルにし、リクエスト内容によってそれなりに適切なステータスコードを返せる netcat を含むスクリプトが出来ました。
以下が netcat_2_file.sh の全文です・
```bash:netcat_2_file.sh
#!/bin/bash

file_name=$(date "+%s") # 重複しないように unixtime でHTTPリクエスト用ファイルを作る。

while read -r line; do

	trline=$(echo $line | tr -d "\r\n") # 行末から \r\n を取り除く
	echo "$trline" # それを file_name ファイルへ書き込めるよう echo

	content_reg='Content-Length:\s(.*?)'
	[[ "$trline" =~ $content_reg ]] && # 正規表現 'Content-Length:\s(.*?)' と '=~' 演算子を用い、'Content-lengtn: 数字' 形式の行であるかどうかを判定し
	CONTENT_LENGTH=$(echo $trline | sed -E "s/$content_reg/\1/") # その行ならば数字だけを取り出して CONTENT_LENGTH 変数に入れる。

	[ -z "$trline" ] && break # ヘッダとボディの間の空行でこのループを出る。

done > $file_name # 空行までを file_name ファイルに書き込む。


if [[ $CONTENT_LENGTH -gt 0 ]]; then
	line=$(head -c $CONTENT_LENGTH -) # read 〜 コマンドから head 〜 コマンドへ変更
	body=$line # リクエストのボディ部分を body 変数へ入れる
	echo -n $line >> $file_name # それを file_name ファイルに追加書き込み。
fi

if [ -n "$body" ]; then # ボディがあるならば
	printf "HTTP/1.1 202 Accepted\r\n\r\n" > response # ステコーで200を返し
else
	printf "HTTP/1.1 204 No Content\r\n\r\n" > response # でなければ 204 を返す。
fi

# while :; do cat response | netcat-lN 3000 | bash netcat_2_file.sh ; done
```

## 参考記事
[ 1 Building a Web server in Bash, part I - sockets ](https://dev.to/leandronsp/building-a-web-server-in-bash-part-i-sockets-2n8b)
[ 2 Building a Web server in Bash, part II - parsing HTTP ](https://dev.to/leandronsp/building-a-web-server-in-bash-part-ii-parsing-http-14kg)
[ 3 Building a Web server in Bash, part III - Login ](https://dev.to/leandronsp/building-a-web-server-in-bash-part-iii-login-a03)
[ 4 Building a Web server in Bash, the grand finale ](https://dev.to/leandronsp/building-a-web-server-in-bash-the-grand-finale-n14)