LoginSignup
1
0

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

Last updated at Posted at 2024-03-08

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

ちなみに、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
    }
}

通信方式として以下が指定できます。

  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用のモジュールです。

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

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にしています。

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

index.js
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

ソースコード全体

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デバイス制御

以上

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0