Kinx ライブラリ - Net.Http

# Network - Http

## はじめに

**「見た目は JavaScript、頭脳(中身)は Ruby、（安定感は AC/DC）」** でお届けしているスクリプト言語 [Kinx](https://github.com/Kray-G/kinx)。言語はライブラリが命。ということでライブラリの使い方編。 

今回は Network(Http) です。

* 参考
    * 最初の動機 ... [スクリプト言語 KINX（ご紹介）](https://qiita.com/Kray-G/items/ca08b6fb40d15dd0ec76)
        * 個別記事へのリンクは全てここに集約してあります。
    * リポジトリ ... [https://github.com/Kray-G/kinx](https://github.com/Kray-G/kinx)
        * Pull Request 等お待ちしております。 

libcurl を抱えているので Http 以外も作れるはずだが、まずは一番欲しいのは Http/Https でしょう。

## HTTP/HTTPS リクエスト

各メソッドの意味は後で記載することとして、まずは使い方から。

### Http クライアント

Http クライアント・オブジェクトは `Net.Http` クラスを使用する。

```javascript
var http = new Net.Http();
```

### HTTP メソッド

`head`、`get`、`post`、`put`、`delete` をサポート。

#### HTTP HEAD - `head`

`HEAD` メソッドで情報を取得する。SSL に関しては一先ず false にして実行。

```javascript
var http = new Net.Http();
http.sslVerifyPeer(false);
http.sslVerifyHost(false);
var r = http.head("https://docs.ruby-lang.org/ja/latest/class/Range.html");
if (r.code == 200) {
    System.println(r.toJsonString(true));
}
```

コールバック関数を指定することもできる。データは取得できたデータ分だけ毎回コールバックされる。その動作はメソッドに関わらず全て同じ動作をする。

また、データは単なるバイト列として扱われるため、日本語などのマルチバイト文字が混じる場合には区切り位置が文字コードの区切りとぴったりあった場所で区切られるとは限らないことに注意。なので、以下のように直接出力した場合、日本語が混じると変になる可能性がある。ヘッダはまだマシだがボディの場合は基本的にはバッファにためていくのが良いかと。

```javascript
var http = new Net.Http();
http.sslVerifyPeer(false);
http.sslVerifyHost(false);
var r = http.head("https://docs.ruby-lang.org/ja/latest/class/Range.html", &(data) => {
    System.print(data);
});
if (r.code == 200) {	
    ...
}
```

コールバックでの取得結果は以下の通り。

```
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 48121
Server: nginx/1.14.2
Content-Type: text/html
Last-Modified: Wed, 15 Apr 2020 00:16:18 GMT
ETag: "5e965252-bbf9"
Cache-Control: public, max-age=43200, s-maxage=172800, stale-while-revalidate=86400, stale-if-error=604800
Accept-Ranges: bytes
Date: Mon, 27 Apr 2020 07:25:51 GMT
Via: 1.1 varnish
Age: 12363
X-Served-By: cache-tyo19947-TYO
X-Cache: HIT
X-Cache-Hits: 1
X-Timer: S1587972352.977171,VS0,VE1
Vary: Accept-Encoding
```

復帰値 `r` にはヘッダ情報をオブジェクト化したオブジェクトが返され、`r.code` に HTTP ステータスコードが格納されており、`r.body` にボディの情報がテキストとして格納されている。この構造は他のメソッドでも同様。HEAD ではボディは無いため `r.body == ""` となる。

上記の例では以下のようになる。

```json
{
    "Last-Modified": "Wed, 15 Apr 2020 00",
    "X-Cache-Hits": "1, 1",
    "body": "",
    "Server": "nginx/1.14.2",
    "code": 200,
    "Date": "Mon, 27 Apr 2020 07",
    "Via": "1.1 varnish",
    "X-Cache": "HIT, HIT",
    "Content-Type": "text/html",
    "Cache-Control": "public, max-age=43200, s-maxage=172800, stale-while-revalidate=86400, stale-if-error=604800",
    "X-Timer": "S1587972352.977171,VS0,VE1",
    "ETag": "\"5e965252-bbf9\"",
    "Vary": "Accept-Encoding",
    "Content-Length": 48121,
    "Accept-Ranges": "bytes",
    "X-Served-By": "cache-tyo19947-TYO",
    "Connection": "keep-alive",
    "Age": 12363
}
```

`head` 以外のメソッドでは `body` にボディ・データがテキストの形で格納される。なので、JSON 形式で受け取る場合などは `JSON.parse(r.body)` と自分でパースする必要があるので注意。

#### HTTP GET - `get`

`GET` メソッドで情報を取得する。コールバックを指定しなければ全てを受信し、情報を格納したオブジェクトを返す。`r.body` は文字列なので、JSON 形式等で受信した場合は自分で `JSON.parse(r.body)` 等を行う。

```javascript
var http = new Net.Http();
http.sslVerifyPeer(false);
http.sslVerifyHost(false);
var r = http.get("https://docs.ruby-lang.org/ja/latest/class/Range.html");
if (r.code == 200) {
    System.println(r.toJsonString(true));
}
```

応答はこんな感じ。

```json
{
    "Last-Modified": "Wed, 15 Apr 2020 00",
    "X-Cache-Hits": 1,
    "body": "<!DOCTYPE html>\n<html lang=\"ja-JP\">\n<head>\n ...省略(長い)...",
    "Server": "nginx/1.14.2",
    "code": 200,
    "Date": "Thu, 30 Apr 2020 12",
    "Via": "1.1 varnish",
    "X-Cache": "HIT",
    "Content-Type": "text/html",
    "Cache-Control": "public, max-age=43200, s-maxage=172800, stale-while-revalidate=86400, stale-if-error=604800",
    "X-Timer": "S1588250498.519558,VS0,VE1",
    "ETag": "\"5e965252-bbf9\"",
    "Vary": "Accept-Encoding",
    "Content-Length": 48121,
    "Accept-Ranges": "bytes",
    "X-Served-By": "cache-tyo19929-TYO",
    "Connection": "keep-alive",
    "Age": 102
}
```


コールバックはヘッダーとボディそれぞれ設定できる。

```javascript
http.get("https://docs.ruby-lang.org/ja/latest/class/Range.html", {
    header: &(data) => {
        ...
    },
    body: &(data) => {
        ...
    },
});
```

コールバックに直接関数オブジェクトを指定した場合、ボディのコールバックとして動作する。

```javascript
http.get("https://docs.ruby-lang.org/ja/latest/class/Range.html", &(data) => {
    ... // ボディ・データが細切れに来る。
});
```

#### HTTP POST - `post`

`POST` メソッドを発行する。POST するデータは `Http#setPostData` メソッドで設定する。その他、コールバック等は `GET` と同じ。

`POST` はなかなか試す場所が無いのだが、Ruby で以下のサーバを立ち上げてアクセスしてみる。エラーはするが、アクセスされていることは確認できる。

```javascript
ruby -rwebrick -e 'WEBrick::HTTPServer.new(:DocumentRoot => "./", :Port => 8000).start'
```

アクセスしてみる。

```javascript
var http = new Net.Http();
http.setPostData("POST-DATA");
http.post("http://localhost:8000/index.html", System.print);
```

GET と同じようにボディがコールバックされてくるため、以下の応答となる。

```
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<HTML>
  <HEAD><TITLE>Not Found</TITLE></HEAD>
  <BODY>
    <H1>Not Found</H1>
    `/index.html' not found.
    <HR>
    <ADDRESS>
     WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29) at
     localhost:8000
    </ADDRESS>
  </BODY>
</HTML>
```

サーバーのログは以下の通り。

```
[2020-04-21 17:33:53] ERROR `/index.html' not found.
::1 - - [21/Apr/2020:17:33:53 DST] "POST /index.html HTTP/1.1" 404 280
- -> /index.html
```

#### HTTP PUT - `put`

`PUT` は `POST` と同じで、メソッド名だけ `PUT` に変更して実行する。

```javascript
var http = new Net.Http();
http.setPostData("PUT-DATA");
http.put("http://localhost:8000/index.html", System.print);
```

#### HTTP DELETE - `delete`

`DELETE` も `GET` と同じで、メソッド名だけ `DELETE` に変更して実行する。

```javascript
var http = new Net.Http();
http.delete("http://localhost:8000/index.html", System.print);
```

### verbose

`Http#setDebug(callback)` を使ってデバッグ情報を取得することができる。以下はデバッグ情報だけ出力させる例。

```javascript
var http = new Net.Http();
http.setDebug(System.print);
http.delete("http://localhost:8000/index.html");
```

Curl の Verbose 情報を見ることができる。

```
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> DELETE /index.html HTTP/1.1
Host: localhost:8000
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 405 Method Not Allowed
< Content-Type: text/html; charset=ISO-8859-1
< Server: WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29)
< Date: Tue, 21 Apr 2020 08:49:07 GMT
< Content-Length: 302
< Connection: close
<
* Closing connection 0
```

ちゃんと `DELETE` メソッドが発行されてますね。

さらに、`setDebugDetail()` メソッドで true を指定すると、送受信したデータを表示することも可能。データ量が多くなるので、適宜コントロールすること。

```javascript
var http = new Net.Http();
http.setDebugDetail(true);
http.setDebug(System.print);
http.delete("http://localhost:8000/index.html");
```

こんな感じになる。

```
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> DELETE /index.html HTTP/1.1
Host: localhost:8000
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 405 Method Not Allowed
< Content-Type: text/html; charset=ISO-8859-1
< Server: WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29)
< Date: Tue, 21 Apr 2020 09:09:27 GMT
< Content-Length: 302
< Connection: close
<
> Recv data, 0000000302 bytes (0x0000012e)
0000: 3c 21 44 4f 43 54 59 50 45 20 48 54 4d 4c 20 50 <!DOCTYPE HTML P
0010: 55 42 4c 49 43 20 22 2d 2f 2f 57 33 43 2f 2f 44 UBLIC "-//W3C//D
0020: 54 44 20 48 54 4d 4c 20 34 2e 30 2f 2f 45 4e 22 TD HTML 4.0//EN"
0030: 3e 0a 3c 48 54 4d 4c 3e 0a 20 20 3c 48 45 41 44 >.<HTML>.  <HEAD
0040: 3e 3c 54 49 54 4c 45 3e 4d 65 74 68 6f 64 20 4e ><TITLE>Method N
0050: 6f 74 20 41 6c 6c 6f 77 65 64 3c 2f 54 49 54 4c ot Allowed</TITL
0060: 45 3e 3c 2f 48 45 41 44 3e 0a 20 20 3c 42 4f 44 E></HEAD>.  <BOD
0070: 59 3e 0a 20 20 20 20 3c 48 31 3e 4d 65 74 68 6f Y>.    <H1>Metho
0080: 64 20 4e 6f 74 20 41 6c 6c 6f 77 65 64 3c 2f 48 d Not Allowed</H
0090: 31 3e 0a 20 20 20 20 75 6e 73 75 70 70 6f 72 74 1>.    unsupport
00a0: 65 64 20 6d 65 74 68 6f 64 20 60 44 45 4c 45 54 ed method `DELET
00b0: 45 27 2e 0a 20 20 20 20 3c 48 52 3e 0a 20 20 20 E'..    <HR>.
00c0: 20 3c 41 44 44 52 45 53 53 3e 0a 20 20 20 20 20  <ADDRESS>.
00d0: 57 45 42 72 69 63 6b 2f 31 2e 34 2e 32 20 28 52 WEBrick/1.4.2 (R
00e0: 75 62 79 2f 32 2e 35 2e 31 2f 32 30 31 38 2d 30 uby/2.5.1/2018-0
00f0: 33 2d 32 39 29 20 61 74 0a 20 20 20 20 20 6c 6f 3-29) at.     lo
0100: 63 61 6c 68 6f 73 74 3a 38 30 30 30 0a 20 20 20 calhost:8000.
0110: 20 3c 2f 41 44 44 52 45 53 53 3e 0a 20 20 3c 2f  </ADDRESS>.  </
0120: 42 4f 44 59 3e 0a 3c 2f 48 54 4d 4c 3e 0a       BODY>.</HTML>.
* Closing connection 0
```

`setDebugDetail()` のオプションに `{ hex: false }` を指定すると、16進データは表示しない形式で取得することもできる。

```javascript
var http = new Net.Http();
http.setDebugDetail(true, { hex: false });
http.setDebug(System.print);
http.delete("http://localhost:8000/index.html");
```

16進ダンプが無くなる。

```
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> DELETE /index.html HTTP/1.1
Host: localhost:8000
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 405 Method Not Allowed
< Content-Type: text/html; charset=ISO-8859-1
< Server: WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29)
< Date: Tue, 21 Apr 2020 09:09:48 GMT
< Content-Length: 302
< Connection: close
<
> Recv data, 0000000302 bytes (0x0000012e)
0000: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">.<HTML>.  <HEAD
0040: ><TITLE>Method Not Allowed</TITLE></HEAD>.  <BODY>.    <H1>Metho
0080: d Not Allowed</H1>.    unsupported method `DELETE'..    <HR>.
00c0:  <ADDRESS>.     WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29) at.     lo
0100: calhost:8000.    </ADDRESS>.  </BODY>.</HTML>.
* Closing connection 0
```

### Fiber

Fiber を使ってデータをループで処理させることもできる。EventMachine 的な使い方を想定しているが、まだ EventMachine は無いので今後の検討項目。

```javascript
var http = new Net.Http();
http.sslVerifyPeer(false);
http.sslVerifyHost(false);
var fiber = new Fiber(&{
	http.get("https://docs.ruby-lang.org/ja/latest/class/Range.html", &(data) => {
		yield data;
	});
});
var total = "";
while (true) {
	var d = fiber.resume();
	break if (!fiber.isAlive());
	total += d;
	System.println("read %d bytes, total %d bytes" % d.length() % total.length());
}
System.println(total);
```

こんな感じで取得できます。

```
read 1371 bytes, total 1371 bytes
read 1371 bytes, total 2742 bytes
read 1371 bytes, total 4113 bytes
read 1371 bytes, total 5484 bytes
read 1371 bytes, total 6855 bytes
read 1371 bytes, total 8226 bytes
read 1371 bytes, total 9597 bytes
read 1371 bytes, total 10968 bytes
read 1371 bytes, total 12339 bytes
read 1371 bytes, total 13710 bytes
read 1371 bytes, total 15081 bytes
read 1371 bytes, total 16452 bytes
read 1371 bytes, total 17823 bytes
read 1371 bytes, total 19194 bytes
read 1371 bytes, total 20565 bytes
read 1371 bytes, total 21936 bytes
read 1371 bytes, total 23307 bytes
read 1371 bytes, total 24678 bytes
read 1371 bytes, total 26049 bytes
read 1371 bytes, total 27420 bytes
read 1371 bytes, total 28791 bytes
read 1371 bytes, total 30162 bytes
read 1371 bytes, total 31533 bytes
read 1371 bytes, total 32904 bytes
read 1371 bytes, total 34275 bytes
read 1371 bytes, total 35646 bytes
read 1371 bytes, total 37017 bytes
read 1371 bytes, total 38388 bytes
read 1371 bytes, total 39759 bytes
read 1371 bytes, total 41130 bytes
read 1371 bytes, total 42501 bytes
read 1371 bytes, total 43872 bytes
read 1371 bytes, total 45243 bytes
read 1371 bytes, total 46614 bytes
read 1371 bytes, total 47985 bytes
read 136 bytes, total 48121 bytes
<!DOCTYPE html>
<html lang="ja-JP">
<head>
<!-- Global Site Tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-620926-3"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments)};
  gtag('js', new Date());

  gtag('config', 'UA-620926-3');
</script>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="../syntax-highlight.css">
<link rel="icon" type="image/png" href="../rurema.png">

<link rel="canonical" href="https://docs.ruby-lang.org/ja/latest/class/Range.html">

<title>class Range (Ruby 2.7.0 リファレンスマニュアル)</title>
...(以下省略)
```

### インスタンス・メソッド

`Net.Http` クラス・インスタンスに定義されているメソッドは以下の通り。正直、証明書関係はちゃんとテストできていないが、libcurl のマニュアル通りの実装をしてみた。基本的には対応する CURL オプションを設定しているだけです。何かあれば教えていただけると助かります。

|             メソッド             |                                                                                     意味                                                                                      |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| sslVerifyPeer(tf)                | true, 証明書を `sslSetCaInfo()` メソッド、または `sslSetCaPath()` メソッドで証明ディレクトリを指定する。 false, 左記の検証を行わない。                                        |
| sslVerifyHost(verify)            | true, SSL ピア証明書に一般名が存在するかどうかを調べ、その名前がホスト名と一致することを検証する。false, 左記の検証を行わない。                                               |
| sslSetCaInfo(path)               | 接続先を検証するための証明書を保持するファイル名。 `sslVerifyPeer()` とともに使用する。                                                                                  |
| sslSetCaPath(path)               | 複数の証明書ファイルを保持するディレクトリ。 `sslVerifyPeer()` とともに使用する。                                                                                        |
| setDebugDetail(tf, opts)         | true, デバッグ情報に送受信データを含める。 `opts` として `{ hex: false }` を指定すると送受信データに 16 進ダンプを含めない。                                                  |
| setUserPassword(user, pass)      | 接続に使用するユーザー名とパスワード。                                                                                                                                        |
| setProxy(url)                    | リクエストを経由させる HTTP プロキシの URL。                                                                                                                                  |
| setProxyUserPassword(user, pass) | プロキシに接続するためのユーザー名とパスワード。                                                                                                                              |
| setTimeout(millisec)             | タイムアウトをミリ秒で指定する。                                                                                                                                              |
| setDebug(callback)               | デバッグ情報を受け取るコールバックを指定する。コールバックを指定するとデバッグ情報を取得するよう設定される。                                                                  |
| addHeader(key, value)            | 設定する HTTP ヘッダフィールドを `key` と `value` で指定する。                                                                                                                |
| removeHeader(key)                | `key` で示されるヘッダを削除する。                                                                                                                                            |
| setRedirect(tf, opts)            | サーバーが HTTP ヘッダの一部として送ってくる "Location: " ヘッダの内容をたどる。`opts` として `{ max: n }` を指定するとリダイレクトする回数の上限を設定できる。               |
| setPostData(data)                | `POST` または `PUT` で送信するデータを設定する。                                                                                                                              |
| head(url, callback)              | `HEAD` メソッドを発行する。コールバックを指定しなかった場合、取得したヘッダデータのオブジェクトを返す。                                                                                     |
| get(url, callbacks)              | `GET` メソッドを発行する。コールバックはヘッダ・ボディ用関数、または `{ header: f1, body: f2 }` でそれぞれ指定可能。コールバックを指定しなかった場合、取得したデータのオブジェクトを返す。    |
| post(url, callbacks)             | `POST` メソッドを発行する。コールバックはヘッダ・ボディ用関数、または `{ header: f1, body: f2 }` でそれぞれ指定可能。コールバックを指定しなかった場合、取得したデータのオブジェクトを返す。   |
| put(url, callbacks)              | `PUT` メソッドを発行する。コールバックはヘッダ・ボディ用関数、または `{ header: f1, body: f2 }` でそれぞれ指定可能。コールバックを指定しなかった場合、取得したデータのオブジェクトを返す。    |
| delete(url, callbacks)           | `DELETE` メソッドを発行する。コールバックはヘッダ・ボディ用関数、または `{ header: f1, body: f2 }` でそれぞれ指定可能。コールバックを指定しなかった場合、取得したデータのオブジェクトを返す。 |

### おわりに

ネットワーク系は色々使い道があるので今後充実させたいカテゴリー。まずは一番使いたい HTTP を用意してみました。libcurl ではできないものも含めてサポートしていきたい。SNMP とか、SSH とか。

ではまた次回。