シェルのワンライナーでHTTPサーバをたてる

Adways Advent Calendar 2017 2日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar_2017


こんにちは!奥村です!

以前に、「awkを使いこなしたい」という記事を書かせて頂きました。

blog.engineer.adways.net

この記事を一言でいうと「一流のシェル芸人になりたい」という内容です。
現在もシェル芸人になるための勉強を続けています。

今回はワンライナーでHTTPサーバをたててみたいと思います。

前準備

今回はリクエストの待ち受け部分に「netcat(nc)」コマンドを使用します。
UNIX標準のコマンドではないため、事前にインストールしておきます。

Redhat系

$ sudo yum install nc

Debian系

$ sudo apt-get install netcat

実行

以下の例では、コマンドを実行するホストに「192.168.33.12」というIPを割り当てました。

$ (echo -e HTTP/1.1 200 OK\n; echo; printf "Hello World\n") | nc -l 8080

このコマンドを実行すると、8080番ポートでHTTPリクエストを待ち受けます。

  • ブラウザからの確認

f:id:AdwaysEngineerBlog:20171204115850p:plain

ブラウザからも表示を確認することができます。
実行したシェル側では

  • シェル
GET / HTTP/1.1
Host: 192.168.33.12:8080
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; ....)
Accept-Language: ja-jp
Accept-Encoding: gzip, deflate
Connection: keep-alive

のようなサーバへのリクエストが表示されています。

$ (echo -e HTTP/1.1 200 OK\n;echo -e Content-Type: text/html; echo; printf "Hello World\n") | nc -l 8080

のようにすると、ステータスコードの一行下にヘッダを書くこともできます。

# 別のターミナルでncコマンドを使って確認
$ echo -en "GET / HTTP/1.1\n\n" | nc localhost 8080
HTTP/1.1 200 OKn
Content-Type: text/html
Hello World

ただ、このコマンドは一度リクエストを受け付けると待ち受けを終了してしまいます。
ですので、while を使ってnc コマンドを再実行するようにします。

$ while :; do (echo -e HTTP/1.1 200 OK\n;echo -e Content-Type: text/html; echo; printf "Hello World\n") | nc -l 8080; done

これで常にリクエストを待ち続けるHTTPサーバができました。

応用

上の例を見てわかる通りレスポンスボディの部分は

printf "Hello World\n"

となっています。

ということはこの部分を変更すれば、任意のコマンドの実行結果をHTTPレスポンスとして返すことができるということです。 blog.engineer.adways.net で作ったシェルの出力結果をJSON形式で出力するコマンドを指定してみます。 (loj(LineOutputJson)コマンドとしてパスを通しておきます。lojの詳細は上記のリンクから確認できます。)

while :; do (echo -e HTTP/1.1 200 OK\n;echo -e Content-Type: text/html; echo; ps aux | grep nc | loj -k 2 -c "2,PID 1,USER 11,CMD") | nc -l 8080; done

これで起動します。
早速curlを実行してみたいと思います。

$ curl 192.168.33.12:8080
{
"22": {"CMD": "[async/mgr]","USER": "root","PID": "22"},"24": {"CMD": "[sync_supers]","USER": "root","PID": "24"},"9164": {"CMD": "nc","USER": "vagrant","PID": "9164"},"9166": {"CMD": "grep","USER": "vagrant","PID": "9166"}}

無事JSON形式でレスポンスを取得することができました。 f:id:AdwaysEngineerBlog:20171204115959p:plain

おまけ

せっかくHTTPリクエストで処理を受け付けれるのだから、URLを使って任意のコマンドを実行し情報がほしくなるものですね。 せっかくなので、一行書いてみることにしました

$ while :; do (sleep 4s; echo -e HTTP/1.1 200 Ok\n; echo; cmd=`cat request | head -n 1 | awk '{ print $2 }' | sed -e 's/\// /g'  | sed -e 's/^\s//' | sed -e 's/pipe/|/g'`; eval $cmd) | nc -l 8080 > request; done

ファイル書き出しと読み込み、変数代入を行っているので、シェル芸的には落第レベルですが、想定していた動作はします。 アドレス以下に「/」区切りでコマンドを与えるとコマンドを実行し、その結果がレスポンスとして返ってきます。

$ curl 192.168.33.12:8080/ps/aux/pipe/grep/nc/
root        22  0.0  0.0      0     0 ?        S    Nov27   0:00 [async/mgr]
root        24  0.0  0.0      0     0 ?        S    Nov27   0:04 [sync_supers]
vagrant   9890  0.0  0.0  11992   696 pts/8    S+   18:30   0:00 nc -l 8080
vagrant   9900  0.0  0.0 103320   908 pts/8    R+   18:30   0:00 grep nc

パイプ(|)をそのままURL部分で渡してしまうとcurl等の実行元でパイプと認識されてしまいます。ですのでパイプは「pipe」を置換するようにしました。

また、ncコマンドで受け取ったリクエストをそのまま使う方法が見つからなかったので一度、ファイル名「request」として書き出すようになっています。
そのため、「2回目の実行時にようやく結果が返ってくる」という風になってしまいました。

一流のシェル芸人ならどうにかしてリクエスト文まで、ワンライナーで取得できるのではないでしょうか。 まだまだ、修行が足りません。

まとめ

コマンドを組み合わせていろいろな事ができるようになってきました。

ですが、僕の目指している「一流のシェル芸人」はこんなものじゃありません。

色んなコマンドがいつでも頭から出てくるように、日々の修行(ワンライナーの問題をすること)を欠かさず、
ls usr/bin/, ls /bin/ , man を継続的に実行していきたいと思います。 最後までご覧頂きありがとうございました。