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 タグを使います。上述の例では
Content-Length: 11
とありますが、これは「hello,world」が11バイトということです。
なのでリクエストのボディ(リクエストの空行より下)を Content-Length タグで示されたバイト数だけ受け取って、そしてそれで終りとみなし、その内容によってステータスコードを curl 側へ送るスクリプトへ netcat から流すようにします。
curl -->-> netcat -->-> シェルスクリプト
↑ ↓
<-<------ステータスコード---
こんなイメージです。
まずはリクエストのヘッダとボディをファイルにします。
#!/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 の全文です・
#!/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
2 Building a Web server in Bash, part II - parsing HTTP
3 Building a Web server in Bash, part III - Login
4 Building a Web server in Bash, the grand finale