netcatでHTTP POSTするときに改行コードでハマった


雰囲気でHTTPを扱っていてハマった話。

環境

  • OS: macOS Sierra
  • Webサーバーの実行環境: Node.js1

netcatでHTTP GETする

HTTPの仕組みを理解するための勉強方法として、netcat2でHTTPプロトコルを手打ちするという方法があります。

server01.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('working!');
});

app.listen(3000);

このようなWebサーバーを立てたとき、curlコマンドでアクセスすると次のようになります。

$ curl localhost:3000
working!

では、これと同じことをnetcat(以降nc)を使ってやってみましょう。

$ nc localhost 3000
GET / HTTP/1.1

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 8
ETag: W/"8-8rFUSOp1Q66VQKob7hAkNyUVaiI"
Date: Wed, 05 Feb 2020 16:01:00 GMT
Connection: keep-alive

working!

GET / HTTP/1.1 の部分をキーボード入力すると、 HTTP/1.1 200 OK で始まる数行の文字列が返され、最後に working! の文字列が表示されました。

という感じで、なんとなくHTTPの雰囲気を味わうことができます。

netcatでHTTP POSTする

ファイルをアップロードできる簡易的なWebサーバーを立てます。

server02.js
const express = require('express');
const path = require('path');
const multer = require('multer');

const upload = multer({ dest: path.resolve('tmp') });
const app = express();

app.get('/form', (req, res) => {
  res.send(`<html>

  <body>
    <form method="POST" action="/upload" enctype="multipart/form-data">
      <input type="file" name="file"><br>
      <input type="submit" value="upload">
    </form>
  </body>

  </html>`);
});

app.get('/download/:fileName', (req, res) => {
  const fileName = req.params.fileName;
  res.download(path.resolve('tmp', fileName));
});

app.post('/upload', upload.single('file'), (req, res) => {
  res.send({ download: '/download/' + req.file.filename });
});

app.listen(3000);

まずは、試しにブラウザからファイルをアップロードしてみます。

このときの通信内容をWireshark3でキャプチャすると次のようになります。

なるほど...こんな感じの内容で手打ちすればファイルがPOSTできそうです。
ということで、上の画像の赤色の部分の文字列をコピペして、ncコマンドに突っ込んでみます。

$ nc localhost 3000
POST /upload HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 196
Cache-Control: max-age=0
Origin: http://localhost:3000
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryGjg4YppOrQq8zAjm
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Referer: http://localhost:3000/form
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8

------WebKitFormBoundaryGjg4YppOrQq8zAjm
Content-Disposition: form-data; name="file"; filename="hoge.txt"
Content-Type: text/plain

hello, world!

------WebKitFormBoundaryGjg4YppOrQq8zAjm--

(反応が返ってこなかったので何度かReturn連打)




HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 352
Date: Wed, 05 Feb 2020 16:56:40 GMT
Connection: keep-alive

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
~~以下省略~~

コピペした内容を、ncコマンドに突っ込んでみたものの、反応なし。その後何度かReturnキーを押すと Internal Server Error が返りました。

改行コードがCRLFからLFに変わってしまった

ncコマンドに突っ込んだ方は改行コードがCRLFからLFに変わってしまったため、 Content-Length が実際より長くなってしまったようです。

↓ ブラウザからアップロードした時のWiresharkのキャプチャ結果(16進数ダンプ)

↓ ncコマンド実行時のWiresharkのキャプチャ結果(16進数ダンプ)

改行が 0d 0a (CRLF) から 0a (LF) に変わっていました...
これだと一行あたり1byte短くなってしまう...

Content-Length を短くして再度実行

Content-Length を189にして再度ncコマンドに突っ込んでみます。

$ nc localhost 3000
POST /upload HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 189
~中略~
Accept-Language: ja,en-US;q=0.9,en;q=0.8

------WebKitFormBoundaryBR5buZN29ZTV1NcQ
Content-Disposition: form-data; name="file"; filename="hoge.txt"
Content-Type: text/plain

hello, world!

------WebKitFormBoundaryBR5buZN29ZTV1NcQ--
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 352
Date: Wed, 05 Feb 2020 17:35:27 GMT
Connection: keep-alive

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
~~以下省略~~

今度はすぐに反応が返ってきましたが、Internal Server Error ...
うまくいかない...

CRLFじゃないとだめらしい

RFC2616の3.7.2 Multipart Typesに次のような記述がありました...

https://tools.ietf.org/html/rfc2616#section-3.7.2
The message body is itself a protocol element and MUST therefore use only CRLF to represent line breaks between body-parts.

ということなので、改行をLFからCRLFに置換したものを、ncコマンドに突っ込んでみました。

$ perl -pe 's/\n/\r\n/' << EOS > reqest.txt
POST /upload HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 196
Cache-Control: max-age=0
Origin: http://localhost:3000
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBR5buZN29ZTV1NcQ
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Referer: http://localhost:3000/form
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8

------WebKitFormBoundaryBR5buZN29ZTV1NcQ
Content-Disposition: form-data; name="file"; filename="hoge.txt"
Content-Type: text/plain

hello, world!

------WebKitFormBoundaryBR5buZN29ZTV1NcQ--

EOS
$ cat reqest.txt - | nc localhost 3000
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 57
ETag: W/"39-ItS2HTg6ed8Zw1CrGzQL3ABlypc"
Date: Wed, 05 Feb 2020 18:38:11 GMT
Connection: keep-alive

{"download":"/download/bd29989fa6ae794857ba429ced286f3f"}
^C
$ curl localhost:3000/download/bd29989fa6ae794857ba429ced286f3f
hello, world!
  • \n -> \r\n の置換はsedだとうまくいかなかったのでperlを使った
  • 一旦ファイルに書き出して cat reqest.txt - をしているのは、パイプの終端に達した時にncコマンドがレスポンスを受け取らずに接続を切ってしまう挙動をするため...これも結構ハマった

ちょっとぎこちないけど、まあ動いたのでよしとしましょう。

※ちなみに、macOSだとこのように問題になりますが、WindowsだとOSの改行コードがCRLFなので何も問題は起きません。