bashで最低限のHTTPサーバを作る

2024-11-27 21:07

こんにちは。 非常に多忙な時期を脱したので少し気が楽です。 でも次の学会まであんまり時間はないので、次の論文を書いてはいます。 これは主な時間泥棒なのですが、 .zshrc をいじるのってなんであんなに楽しいんでしょう。

さて、今回のテーマはbashで最低限のHTTPサーバを作る、です。 私は大体のケースで python -m http.server を使って適当にHTTPサーバを起動し、カレントディレクトリの内容をブラウザから確認したりします。

しかし、限られた環境にSSHをしないといけないことがあります。 まぁ今時Pythonが入ってない環境も珍しくなりつつあり、人間がログインするような環境では大体のケースで簡単にHTTPサーバを起動できますが、それもなんだか無駄な感じもします。 そのようなときには、scpコマンドを使ってファイルのやり取りをしたりしますが、scpコマンドでパスを入力するのって面倒だったりします。

そこで本記事では、コマンドにファイルへのパスを渡すことで、最低限の環境でもHTTPサーバとして指定されたファイルのダウンロードができるようになる、bashで使えるシェルスクリプトを記述しました。 SSH経由でのHTTPサーバへのアクセスは、port forwardingを利用することでできるようになります。

必要条件

この記事の方法でHTTPサーバを作るための要件を以下に挙げます。

  • bash(やそれに準ずるシェル)が使える
  • 以下に挙げるコマンドが使える
    • nc
    • wc
    • awk
    • basename (オプション)

本当に最小限の環境で動かせる構成です。 Pythonとか使いません。 とにかく、「人間がログインすることを想定された、最低限の環境で動く」を目指します。

ソースコード

先に完成したプログラムのソースコードを以下に示します。

dl() {
    echo "http://localhost:${ssh_dl_port}/"
    while true
    do
        cat << EOF | nc -l "$ssh_dl_port" > /dev/null
HTTP/1.1 200 Ok
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="$(basename "$1")"
Content-Length: $(wc -c "$1" | awk '{ print $1 }')

$(cat "$1")
EOF
    done
}

このシェルスクリプトを実行することで、 dl [ファイル名] でファイルのダウンロードができるHTTPサーバの出来上がりです。

解説

この節では、上に挙げたソースコードの各要素について解説していきます。

nc コマンド

nc コマンドは、netcatの略で、簡単に標準入出力とTCP / UDPの通信をpipeすることができます。 nc コマンドはクライアントとしてもサーバとしても振る舞うことができ、データの送受信が容易に行えます。

上にあげたコマンドでは、 cat << EOF | nc -l "$ssh_dl_port" > /dev/null という行があります。 この行では、catの標準出力を nc コマンドにpipeし、 nc -l "$ssh_dl_port"ssh_dl_port 変数に定められたポートをlistenし、このポートに接続があれば、pipeで入力された内容を送信します。 nc コマンドはリモートのクライアントから送られてきた内容を標準出力に出力します。 これは今回の用途ではHTTPリクエストがずらっと流れてしまうので、 /dev/null に流して無視します。

cat コマンド

cat コマンドは、 cat << EOF から始め、次の行以降に内容を記述し、 EOF で締めくくることで、その内容を標準出力に出力することができます。 上にあげたプログラムでは、HTTPプロトコルをそのまま書いています。

HTTPヘッダ

HTTPでは空行の前までがヘッダとなり、さまざまな情報を送ることができます。

HTTP/1.1 200 Ok はHTTPのリクエストが正常に処理されたこと、 Content-Type: application/octet-stream は送信するコンテンツの種類を示します。

application/octet-stream はさまざまなファイルタイプに利用することができ、一般にこれを受け取ったブラウザの挙動は、送信されたbodyのダウンロードになります。

Content-Disposition: attachment; filename="$(basename "$1")" では、ファイルが埋め込み( inline )ではなく attachment として扱われてほしいことと、そのファイル名が記述されています。 filename="$(basename "$1")" の記述は、 $1 、すなわち関数の一つ目の引数で指定されたパスのファイル名のみ取り出して送信しています。

Content-Length: $(wc -c "$1" | awk '{ print $1 }') が一番難しいところだと思います。 Content-Length を指定し送信するbodyの大きさを示すことで、ダウンロードの途中経過などが確認できるようになります。 wc コマンドはファイルの大きさなどを取得するコマンドで、その結果を awk に渡して、出力の一つ目を取り出すことで、ファイルサイズが取得できるということになっています。

HTTP body

ここまで来ればもう簡単です。 HTTPでクライアントにファイルの内容を流します。 具体的には、 cat コマンドで、そのファイルを出力すればいいです。 この方法で、テキストファイルのみならず、バイナリファイルも送信することができます。

おわりに

本記事では、bashと最低限のコマンドを用いて、一つのファイルのみを配信するHTTPサーバを作りました。 これにより、ごく最低限のコマンドのみ使える環境でも、HTTPサーバとして単一のファイルをserveすることができます。 私は実際にSSHのport forwardingとこの dl コマンドを用いて、ファイルのダウンロードをしたりします。

なんか楽なサーバとのファイルのやり取りの仕方とかあれば、そういうのも知りたいので、ぜひお教えください。 (VSCodeのようなGUIアプリケーションは使いづらくて使っていません。全部キーボードだけで操作するのに慣れ過ぎちゃって……)