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)だけを実装すればよいので楽になります。
ちなみに、ESP32とブリッジサーバとの間はHTTPなので、安全ではないので、信頼できるローカルネットワーク内での利用にとどめましょう。
ブリッジサーバは、WebAPIサーバとして用意しますので、クライアント側はESP32以外でも通信できます。
ソースコードもろもろは以下のGitHubに上げておきました。
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
}
}
通信方式として以下が指定できます。
- get
- post_json
- post_form-data
- 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用のモジュールです。
①の方は以下の部分に実装しています。
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にしています。
②の方は以下の部分に実装しています。
app.use(express.json({
limit: MAX_DATA_SIZE
}));
app.post('/agent', async (req, res) => {
・・・・
});
あまり細かく説明する必要はないですね。
Content-Typeに従った通信には、以下のnpmモジュールを使わせていただいています。
ちなみに、v2系を使っています。
起動方法
以下からZIPでダウンロードします。
適当なフォルダで以下を実行します。
> unzip http_bridge_agent-master.zip
> cd http_bridge_agent-master
> npm install
> node 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する場合の例を示します。
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デバイス制御
以上