0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2024-01-03

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 -->-> シェルスクリプト
 ↑             ↓
   <-<------ステータスコード---

こんなイメージです。
まずはリクエストのヘッダとボディをファイルにします。

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 の全文です・

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

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?