ESP32で気兼ねなくHTTPS通信する

ESP32でHTTPS通信はできなくはないですが、かなりメモリを消費するため、結構不安定だったりします。
また、HTTP通信といっても、GETやPOSTの中でも、単にJSONをBodyに含めたものだけでなく、multipart/form-dataであったり、application/x-www-form-urlencodedであったり、送信方式にも種類があります。それをESP32の中でそれぞれ実装するのは面倒です。

そこで、HTTP通信をHTTPS通信にブリッジしてくれるサーバを別途用意することで、ESP32ではTLS通信を意識する必要がないようにします。
また、ESP32とブリッジサーバとの通信方式はJsonをボディとしたHTTP POST通信に限定し、ブリッジサーバにて各種Content-Typeに合わせてその先のHTTPS通信をしてくれるようにすれば、HTTP Post(JSON)だけを実装すればよいので楽になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/20cb959a-0def-1ddb-5585-fd4ebb366e77.png)

ちなみに、ESP32とブリッジサーバとの間はHTTPなので、安全ではないので、信頼できるローカルネットワーク内での利用にとどめましょう。
ブリッジサーバは、WebAPIサーバとして用意しますので、クライアント側はESP32以外でも通信できます。

ソースコードもろもろは以下のGitHubに上げておきました。

poruruba/http_bridge_agent

https://github.com/poruruba/http_bridge_agent

## HttpBridgeサーバの仕様

ブリッジサーバは、2つのエンドポイントを用意します。

①受信したリクエストをそのままその先のHTTPSサーバに転送し、受け取ったレスポンスをそのままESP32に戻します。
②ESP32からはHTTP Post(JSON)でリクエストを受け付け、リクエストの内容に応じてContent-Typeに合わせた通信をその先のHTTPSサーバと行います。

以降、ブリッジサーバのURLを http://bridge.sample.com:30080 とし、最終的に通信したいその先HTTPSサーバを https://target.net/webapi とします。

### ①受信したリクエストをそのまま転送

エンドポイント： /

受信したパラメータやボディのすべてをHTTPSサーバに転送します。
URLのプロトコルとホスト名だけ差し替えます。
その場合、ESP32から見たときに、以下のパラメータで通信します。

```
URL：http://bridge.sample.com:30080/
Header
 target_host: https://target.net/webapi
```

上記だけ設定すれば、それ以外のQueryStringやBodyはそのまま接続先のHTTPSサーバに転送されます。

### ②受信パラメータに合わせて転送

エンドポイント： /agent

通信プロトコルは、HTTP POST(JSON) 固定です。
通信設定としては、まず基本として以下を指定します。

```
URL：http://bridge.sample.com:30080/agent
Header
 target_host: https://target.net/webapi
 target_type: 通信方式
Body(JSON)
　{
    headers: {
        // HTTP Headers
    },
    qs: {
        // QueryStrings
    },
    params: {
        // Parameters
    },
    body:{
        // Body
    }
}
```

通信方式として以下が指定できます。
1. get
2. post_json
3. post_form-data
4. post_x-www-form-urlencoded

※ 上記で足りなかったら足していこうと思います。

### 1. get

HTTP Get で通信します。
paramsとbodyは使いません。
headersに、HTTP Headerに含めたい内容をJSON形式で指定します。
qsに、key:value 形式で値を指定すれば、ブリッジサーバで、URLエンコードしてQueryStringとしてtarget_hostにくっつけて通信します。

Bodyの例
```
{
    headers: {
        Authorization: "Bearer XXXX"
    },
    qs: {
        param1: "value1"
    }
}
```

実際の通信内容

```
URL : https://target.net/webapi?params1=value1
Headers:
 Authorization: Bearer XXXX
```

### 2. post_json

HTTP Post(JSON) で通信します。
paramsは使いません。
headersは、getの時と同じです。
bodyに、Bodyに含めたい内容をJSON形式で指定します。

Bodyの例
```
{
    headers: {
        Authorization: "Bearer XXXX"
    },
    qs: {
        param1: "value1"
    }
    body: {
        param2: "value2"
    }
}
```

実際の通信内容
```
URL : https://target.net/webapi?params1=value1
Headers:
 Authorization: Bearer XXXX
 Content-Type: application/json
Body:
{
    param2: "value2"
}
```

### 3. post_form-data

HTTP Postですが、```multipart/form-data``` 形式でHTTPSサーバと通信します。
headersやqsは、getの時と同じです。
bodyは使いません。
multipartとして含めたい内容は、paramsに指定します。そうすると、ブリッジサーバで、```multipart/form-data形```式にしてHTTPS通信してくれます。

Bodyの例
```
{
    headers: {
        Authorization: Bearer XXXX
    },
    qs: {
        param1: "value1"
    }
    params: {
        param3: "value3"
    }
}
```

実際の通信内容

```
URL : https://target.net/webapi?params1=value1
Headers:
 Authorization: "Bearer XXXX"
 Content-Type: multipart/form-data; boundary=hogehoge;
Body:
--hogehoge
Content-Disposition: form-data; name="param3"

value3
--hogehoge--
```

### 4. post_x-www-form-urlencoded

HTTP Postですが、```application/x-www-form-urlencoded``` 形式でHTTPSサーバと通信します。
headersやqsは、getの時と同じです。
bodyは使いません。
```x-www-form-urlencoded```として含めたい内容は、paramsに指定します。そうすると、ブリッジサーバで、```application/x-www-form-urlencoded```形式にしてHTTPS通信してくれます。

Bodyの例
```
{
    headers: {
        Authorization: "Bearer XXXX"
    },
    qs: {
        param1: "value1"
    }
    params: {
        param3: "value3"
    }
}
```

実際の通信内容

```
URL : https://target.net/webapi?params1=value1
Headers:
 Authorization: Bearer XXXX
 Content-Type: application/x-www-form-urlencoded
Body:
param3=value3
```

## HttpBridgeサーバの中身

特に複雑なことをしているわけではなく、以下のnpmモジュールを使わせていただきました。
Express用のモジュールです。

https://github.com/villadora/express-http-proxy

①の方は以下の部分に実装しています。

```js:index.js
const proxy = require('express-http-proxy');
const app = express();
app.use(cors());

app.use('/', proxy((req) => {
    const host = req.headers["target_host"];
    if (!host)
        throw "target_host not set";
    var location = new URL(host);
    return location.protocol + "//" + location.host;
}, {
    parseReqBody: false,
    proxyReqPathResolver: function(req) {
        const host = req.headers["target_host"];
        if (!host)
            throw "target_host not set";
        var location = new URL(host);
        return location.pathname + location.search + location.hash;
    }
}));
```

proxyの第一引数が、転送する先のホスト名を指定します。HTTP Headerのtarget_hostに通信したいHTTPSサーバのプロトコルとホスト名が入っているはずなので、それを返すようにしています。これで接続先が切り替わります。
第二引数が、転送する先のパスやquerystringを指定します。HTTP Headerのtarget_hostから取り出します。
あとの処理、すなわちPipe処理は、express-http-proxyモジュールがやってくれます。
```parseReqBody: false``` がありますが、直前で/agent向けの処理でbodyをJSONでパースしているのですが、ここでは不要なのでOffにしています。

②の方は以下の部分に実装しています。

```js:index.js
app.use(express.json({
    limit: MAX_DATA_SIZE
}));

app.post('/agent', async (req, res) => {
・・・・
});
```

あまり細かく説明する必要はないですね。
Content-Typeに従った通信には、以下のnpmモジュールを使わせていただいています。
ちなみに、v2系を使っています。

https://github.com/node-fetch/node-fetch


## 起動方法

以下からZIPでダウンロードします。

https://github.com/poruruba/http_bridge_agent

適当なフォルダで以下を実行します。

```
> unzip http_bridge_agent-master.zip
> cd http_bridge_agent-master
> npm install
> node index.js
```

## ソースコード全体

```js:index.js
const MAX_DATA_SIZE = process.env.MAX_DATA_SIZE || '1mb';

require('dotenv').config();
const proxy = require('express-http-proxy');
const express = require('express');
const cors = require('cors');

const FormData = require('form-data');
const {URL, URLSearchParams} = require('url');
const fetch = require('node-fetch');
const Headers = fetch.Headers;

const app = express();
app.use(cors());
app.use(express.json({
    limit: MAX_DATA_SIZE
}));

app.post('/agent', async (req, res) => {
    console.log('/agent called');
    console.log("body=" + JSON.stringify(req.body));
    try{
        const host = req.headers["target_host"];
        const type = req.headers["target_type"];
        if (!host || !type)
            throw "target_host/type not set";

	    console.log("host=" + host);
	    console.log("type=" + type);
        let headers = make_headers(req.body.headers);
        let response;
        if (type == "get") {
            response = await do_get(host, req.body.qs, headers)
        } else
        if (type == "post_json") {
            response = await do_post(host, req.body.qs, req.body.body, headers);
        } else
        if (type == "post_form-data") {
            response = await do_post_formdata(host, req.body.qs, req.body.params, headers);
        } else
        if (type == "post_x-www-form-urlencoded") {
            response = await do_post_urlencoded(host, req.body.qs, req.body.params, headers);
        } else {
            throw "target_type not invalid";
        }

        response.body.pipe(res);
    }catch(error){
        res.status(500);
        res.json({errorMessage: error.toString() });
    }
});

app.use('/', proxy((req) => {
    const host = req.headers["target_host"];
    if (!host)
        throw "target_host not set";
    var location = new URL(host);
    return location.protocol + "//" + location.host;
}, {
    parseReqBody: false,
    proxyReqPathResolver: function(req) {
        const host = req.headers["target_host"];
        if (!host)
            throw "target_host not set";
        var location = new URL(host);
        return location.pathname + location.search + location.hash;
    }
}));

const port = Number(process.env.PORT) || 30080;
app.listen(port, () => {
    console.log('http PORT=' + port)
})

function make_headers(headers_json) {
    const headers = new Headers();

    if (headers_json) {
        Object.keys(headers_json).forEach(key => {
            if (key == 'target_host' || key == 'target_type')
                return;
            headers.set(key, headers_json[key]);
        });
    }

    return headers;
}

async function do_get(url, qs, headers) {
    var params = new URLSearchParams(qs).toString();
    var searchs = new URL(url).searchParams.toString();
    var postfix = params ? (searchs ? '&' + params : '?' + params) : "";
    return fetch(url + postfix, {
        method: 'GET',
        headers: headers
    });
}

async function do_post_formdata(url, qs, params, headers) {
    var params = new URLSearchParams(qs).toString();
    var searchs = new URL(url).searchParams.toString();
    var postfix = params ? (searchs ? '&' + params : '?' + params) : "";
    const body = Object.entries(params).reduce((l, [k, v]) => {
        l.append(k, v);
        return l;
    }, new FormData());

    return fetch(url + postfix, {
        method: 'POST',
        body: body,
        headers: headers
    });
}

async function do_post_urlencoded(url, qs, params, headers) {
    var _params = new URLSearchParams(qs).toString();
    var searchs = new URL(url).searchParams.toString();
    var postfix = _params ? (searchs ? '&' + _params : '?' + _params) : "";
    headers.set('Content-Type', 'application/x-www-form-urlencoded');
    const body = new URLSearchParams(params);

    return fetch(url + postfix, {
        method: 'POST',
        body: body,
        headers: headers
    });
}

async function do_post(url, qs, body, headers) {
    var params = new URLSearchParams(qs).toString();
    var searchs = new URL(url).searchParams.toString();
    var postfix = params ? (searchs ? '&' + params : '?' + params) : "";
    headers.set('Content-Type', 'application/json');

    return fetch(url + postfix, {
        method: 'POST',
        body: JSON.stringify(body),
        headers: headers
    });
}
```

## ESP32のJavascript環境での実行例

本ブリッジサーバを用意しようとしたきっかけは、よく使っているESP32でのJavascript環境で気軽にHTTPS通信するためでした。
ESP32のJavascript環境からLINE Notifyする場合の例を示します。

```js
import * as http from "Http";

http.setHttpBridgeServer("http:// 立 ち 上 げ た HttpBridge サ ー バ の IP アドレ
ス:30080");
const LINE_NOTIFY_ACCESS_TOKEN = "LINE Notify のアクセストークン";
const body = {
    message: "こんにちは"
};
const headers = {
    Authorization: "Bearer " + LINE_NOTIFY_ACCESS_TOKEN
}
const result = http.fetch(http.method_post_urlencode | http.resp_json, "https://notifyapi.line.me/api/notify", null, body, headers);
console.log(JSON.stringify(result));
```

### ESP32のJavascsript実行環境の参考ページ

[M5StackとJavascriptではじめるIoTデバイス制御](https://poruruba.booth.pm/items/3735175)

https://github.com/poruruba/QuickJS_ESP32_IoT_Device_M5Unified

以上