わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

Node.jsで動く,GET・POST両対応の簡単なHTTPサーバを作る

 学生から,メールで問い合わせがありました.
 LbTypingの不具合です.問題作成支援機能*1で,解説文が長くなったため,データベースに登録できておらず,DevToolsのコンソールを見ると,HTTPステータスコードの431(Request Header Fields Too Large)が発生するというのです.
 心当たりはあります.やりとりをすべて,HTTPのGETメソッドで行っています.運用しているサーバのアクセスログにも,攻撃目的ではないけれど長大なパスが登録されているのを見かけます.
 原因が単純なら対策も単純で,やりとりをPOSTメソッドにすればいいのです.いきなり全部を変更すると,不具合発生時にどこが原因か分からなくなりますので,最小限,ということで問題作成支援の登録時と,それを受け取るWebサーバのJavaScriptファイル(httpd.js)に手を加えてみました.手元ではうまく動作しましたが,諸事情によりGitHubにはまだプッシュしていません.
 コーディングの前に,少しばかり調査です.先述のhttpd.jsでは,Expressモジュールを使用しています.それで,GETとPOSTをどう切り替えるのがスマートかと考えながら,検索して,app.getやapp.postが書かれたWebページを読んでいきながら,10分もせずに気づいたのは,httpd.jsではapp.getもapp.postも使用していないという事実でした.「app.use((req, res, next) => ...」と書いて,「const u = Url.parse(req.url, true)」でパラメータを解析して,「u.query.名前」を参照していました.
 GET・POST両対応のWebサーバは,app.getとapp.postに分けなくても作れそう,あとはPOSTのパラメータ獲得だと判断して,もう少し情報収集を行い,コーディングに取りかかりました.
 LbTypingの作業コピーと別のディレクトリで,server.jsというファイルを新規に作成しました.Node.jsで動かし,Expressモジュールを活用します.80番とは異なるポート番号で待ち受けて,curlコマンドなどの問い合わせに応じてパラメータを獲得し,console.logで(すなわちサーバ実行の標準出力に)出力させることとします.
 少しの苦労の末に,完成しました.Gistに置いておきます.WSL2などのシェル上で実行するものとし,nodeとnpmのコマンドもすでに利用可能とします.ファイルの上部に書いたコマンドを実行すればサーバが起動します.HTTPクライアント(Chromecurlなど)からの問い合わせに応じて,パラメータを出力し,「Hello World (GET)」または「Hello World (POST)」を返します.サーバと別のシェルから,パラメータ1つだけのPOSTによるcurlコマンドを実行したときの,サーバとクライアントの出力を以下に示します.

$ node server.js
server listening...
{
  u: Url {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: null,
    query: { ABC: 'xyz' },
    pathname: '/',
    path: '/',
    href: '/'
  }
}
$ curl -v -d ABC=xyz 'http://localhost:8080/'
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 7 out of 7 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 19
< ETag: (略)
< Date: Sat, 19 Nov 2022 21:55:06 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
Hello World (POST)
* Connection #0 to host localhost left intact

 サーバの出力は数行にわたっていますが,その多くは,Urlクラスのインスタンスに含まれているもので,LbTypingのサーバの処理で必要となるのは,queryとpathnameの2つだけです.
 ところで,POSTでもGETと同じように,パスの後ろに「?」のあと「名前=値」を書いて(複数あるなら&でつないで)パラメータを指定できます.その際,リクエストボディに,パスと同じ「名前」があったときにどうするかですが,今回のコードでは,パス(GETのほうの名前)を優先するようにしました.「u.query = { ...req.body, ...u.query };」の行によります.この行のreq.bodyとu.queryを反対にすると,リクエストボディ(POST)優先となります.

*1:授業でアクセスしてもらっているサービスでは,取り除いています.