Deno で MQTT(MQTTS) を動かしてみる

以前からこのブログでは、MQTTでドアセンサーからの通知を受け取ったりということをしてきた。

Node.js互換や、npm の対応も進んでいるので Deno でもできるんだろうなとぼんやり考えてたので、試してみたい。

参考

実装

ブローカーを opifex で立てる

サーバーを aedes を立てるのを試みたがどうにもうまくいかなかったので、別のものを探す。
というわけで、opifex を見つけた。

起動方法は次の通り。

1
$ deno run --allow-net https://deno.land/x/opifex/bin/demoServer.ts

これだけでいい。

クライアント

クライアントは2つ用意して、お互いにメッセージを送り合ってもらう。

クライアント1

client-1.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Client } from "https://deno.land/x/mqtt/deno/mod.ts";

const client = new Client({ url: "mqtt://localhost:1883" });

console.log("connecting");
await client.connect();
console.log("connected");

await client.subscribe("topic/2");

client.on("message", (topic, payload) => {
console.log([topic, new TextDecoder().decode(payload)]);
});

console.log("publishing");
setInterval(async () => {
await client.publish("topic/1", "from publisher1");
}, 2000);

クライアント2

client-2.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Client } from "https://deno.land/x/mqtt/deno/mod.ts";

const client = new Client({ url: "mqtt://localhost:1883" });

console.log("connecting");
await client.connect();
console.log("connected");

await client.subscribe("topic/1");

client.on("message", (topic, payload) => {
console.log([topic, new TextDecoder().decode(payload)]);
});

console.log("publishing");
setInterval(async () => {
await client.publish("topic/2", "from publisher2");
}, 1000);

動作確認

opifex をブローカーとして起動し、クライアントを2つとも起動すると次のようになる。

コンソール1
1
2
3
4
5
6
7
8
9
10
11
12
$ deno run -A .\client-1.ts
connecting
connected
publishing
[ "topic/2", "from publisher2" ]
[ "topic/2", "from publisher2" ]
[ "topic/2", "from publisher2" ]
[ "topic/2", "from publisher2" ]
[ "topic/2", "from publisher2" ]
[ "topic/2", "from publisher2" ]
[ "topic/2", "from publisher2" ]
[ "topic/2", "from publisher2" ]
コンソール2
1
2
3
4
5
6
7
8
9
10
11
$ deno run -A .\client-2.ts
connecting
connected
publishing
[ "topic/1", "from publisher1" ]
[ "topic/1", "from publisher1" ]
[ "topic/1", "from publisher1" ]
[ "topic/1", "from publisher1" ]
[ "topic/1", "from publisher1" ]
[ "topic/1", "from publisher1" ]
[ "topic/1", "from publisher1" ]

想定通りメッセージを相互にやり取りできていることは確認できる。

ブローカーを mosquitto で立てる

MQTT のオープンソース実装の mosquitto は、公式が docker イメージを公開しているので、それで立てることができる。

以下順に用意する。

compose.yaml
1
2
3
4
5
6
7
8
9
version: "3"
services:
mosquitto:
image: eclipse-mosquitto:2.0.15
volumes:
- ./config:/mosquitto/config
ports:
- 1883:1883
- 9001:9001
config\mosquitto.conf
1
2
listener 1883 0.0.0.0
allow_anonymous true

準備ができたので起動。

1
2
3
4
5
6
7
8
9
10
11
12
$ docker compose up
[+] Running 1/0
- Container test415-deno-mqtt-mosquitto-1 Recreated 0.1s
Attaching to test415-deno-mqtt-mosquitto-1
test415-deno-mqtt-mosquitto-1 | 1690550102: mosquitto version 2.0.15 starting
test415-deno-mqtt-mosquitto-1 | 1690550102: Config loaded from /mosquitto/config/mosquitto.conf.
test415-deno-mqtt-mosquitto-1 | 1690550102: Opening ipv4 listen socket on port 1883.
test415-deno-mqtt-mosquitto-1 | 1690550102: mosquitto version 2.0.15 running
test415-deno-mqtt-mosquitto-1 | 1690550102: New connection from 172.19.0.1:60312 on port 1883.
test415-deno-mqtt-mosquitto-1 | 1690550102: New client connected from 172.19.0.1:60312 as mqttts-g3esvnkxcg9 (p2, c1, k60).
test415-deno-mqtt-mosquitto-1 | 1690550103: New connection from 172.19.0.1:60314 on port 1883.
test415-deno-mqtt-mosquitto-1 | 1690550103: New client connected from 172.19.0.1:60314 as mqttts-4xea757ulwk (p2, c1, k60).

クライアント側を確認すると、opifex で通信したのと同様に、クライアント間で通信ができているはず。

ブローカー mosquitto をもう少しセットアップ

先の、config\mosquitto.conf のセットアップは、接続に制限が無い状態になっている。

config\mosquitto.conf
1
2
listener 1883 0.0.0.0
allow_anonymous true <= 誰でもOK

これに制限を入れてみる。
幸い、confのファイルは、すべて公開されているので、それをベースに設定できる。

ユーザー、パスワード制限

mosquitto のパスワード制限にはパスワードファイルを使用する。
そのまま生のファイルは使用できないので、変換が必要だった。

config 以下に password ファイルを用意する。

config/password
1
user1:XXXXX

このファイルを mosquitto が提供するツールで変換する。

1
$ docker compose run mosquitto mosquitto_passwd -U /mosquitto/config/password

変換後は次のようになっている。

config/password(変換後)
1
user1:$7$101$CUaO1jWTtkGLIssf$1pjq3s6r+e3xEZMLq03ZjxBn/fn7hwgOsuUcUO0eAybdNFG7KX1U19Iq6P438xIr9uAWGeWqk2hAoJ5jL8UUAw==

変換できたので適用する。

config\mosquitto.conf
1
2
3
listener 1883 0.0.0.0
#allow_anonymous true
password_file /mosquitto/config/password

mosquitto を再起動すると、クライアントが通信できなくなっている。
今度は、クライアント側のソースを改修する。

client-1.ts(改修)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Client } from "https://deno.land/x/mqtt/deno/mod.ts";

const client = new Client({ url: "mqtt://localhost:1883",
username: "user1", // <= パスワードファイルに記載した内容を設定
password: "XXXXX"
});

console.log("connecting");
await client.connect();
console.log("connected");

await client.subscribe("topic/2");

client.on("message", (topic, payload) => {
console.log([topic, new TextDecoder().decode(payload)]);
});

console.log("publishing");
setInterval(async () => {
await client.publish("topic/1", "from publisher1");
}, 2000);

client-2.ts も同様に改修する。
クライアント間の通信も復活する。

SSL/TLS を導入

証明書認証を設定する際の、mosquitto.conf を、MQTT.ts が公開している。

https://raw.githubusercontent.com/jdiamond/MQTT.ts/master/mosquitto-tls.conf
1
2
3
4
5
6
7
log_type all
connection_messages true

port 8883
cafile ca.crt
certfile localhost.crt
keyfile localhost.key

これを参考に、セットアップを試みる。

openssl が必要なのだが、windows 機なので、セットアップするより汎用的に docker 経由で呼び出してみる。

dockerfile を新規に作成。

1
2
3
4
5
6
7
FROM alpine

RUN apk add --update openssl && \
rm -rf /var/cache/apk/*

RUN mkdir /usr/certs
WORKDIR /usr/certs
compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3"
services:
mosquitto:
image: eclipse-mosquitto:2.0.15
volumes:
- ./config:/mosquitto/config
ports:
- 1883:1883
al-openssl:
build:
context: .
dockerfile: openssl-dockerfile
volumes:
- ./certs:/usr/certs
tty: true

1回ビルドしておく。

1
$ docker compose build

認証局秘密鍵の作成

1
2
3
$ docker compose run al-openssl openssl genrsa -des3 -out ca.key 2048
# => パスワードを確認されるので入れる
# => certs/ca.key が作成される

認証局証明書の発行

1
2
3
4
5
6
7
8
9
10
11
$ docker compose run al-openssl openssl req -new -x509 -days 36500 -key ca.key -out ca.crt
# => パスワードを確認されるので入れる
# => 以下入力要求があるので入れる
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:tokyo
Locality Name (eg, city) []:tokyo
Organization Name (eg, company) [Internet Widgits Pty Ltd]:same
Organizational Unit Name (eg, section) []:same
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:
# => certs/ca.crt が作成される

サーバー秘密鍵の作成

1
2
$ docker compose run al-openssl openssl genrsa -out localhost.key 2048
# => certs/localhost.key が作成される

サーバー証明書の発行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker compose run al-openssl openssl req -new -out localhost.csr -key localhost.key 
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:tokyo
Locality Name (eg, city) []:tokyo
Organization Name (eg, company) [Internet Widgits Pty Ltd]:same
Organizational Unit Name (eg, section) []:same
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
# => certs/localhost.csr が作成される
1
2
3
4
$ docker compose run al-openssl openssl x509 -req -in localhost.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out localhost.crt -days 36500
# => パスワードを確認されるので入れる
Enter pass phrase for ca.key:
# => certs/localhost.crt が作成される

クライアント秘密鍵の作成

1
2
$ docker compose run al-openssl openssl genrsa -out client.key 2048
# => certs/client.key が作成される

クライアント証明書の発行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker compose run al-openssl openssl req -new -out client.csr -key client.key 
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:tokyo
Locality Name (eg, city) []:tokyo
Organization Name (eg, company) [Internet Widgits Pty Ltd]:same
Organizational Unit Name (eg, section) []:same
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
# => certs/localhost.csr が作成される
1
2
3
4
$ docker compose run al-openssl openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 36500
# => パスワードを確認されるので入れる
Enter pass phrase for ca.key:
# => certs/localhost.crt が作成される

各種ファイルが作成されたので、config\mosquitto.conf と compose.yaml を修正。

config\mosquitto.conf
1
2
3
4
5
6
port 8883 0.0.0.0
password_file /mosquitto/config/password

cafile /mosquitto/certs/ca.crt
certfile /mosquitto/certs/localhost.crt
keyfile /mosquitto/certs/localhost.key
compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3"
services:
mosquitto:
image: eclipse-mosquitto:2.0.15
volumes:
- ./config:/mosquitto/config
- ./certs:/mosquitto/certs
ports:
- 1883:1883
al-openssl:
build:
context: .
dockerfile: openssl-dockerfile
volumes:
- ./certs:/usr/certs
tty: true

改めて mosquitto を起動する。がうまくいかない。

うまくいかないので、Windows のローカルに mosquitto をインストール

dockerで起動しているために CN のところで不整合がどうやらあるようで上手くいかない。

仕方が無いので、 Windows 機のローカルに mosquitto を改めてインストール。
先に作った mosquitto.conf を流用して改めて起動した。

1
$ mosquitto.exe -c mosquitto.conf -v

このタイミングでの動作に十全の自信が無くなっていたので、MQTT Explorer を導入。

http://mqtt-explorer.com/

このツールで、ローカルに建てた mosquitto と SSL/TLS で通信ができていることを確認できた。

MQTT.ts が mqtts に対応していなかったらしい。どうやら。

あまりにもうまくいかないので、ドキュメントを読み直すと、次の記述。

Roadmap to 1.0

  • mqtts for deno and node clients

というわけで、MQTT.ts を使い続けるのはどうやら無理であろうことが確定。
そして deno.land/x の登録としては2年ほどメンテがされていないのを確認。
github の最新の更新としては2022年10月の更新で、ロードマップは更新されていなかった。

MQTT.js に切り替えてみる。

client-mqttjs.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const mqtt = require("mqtt");
const fs = require('fs')
const path = require('path')

const client = mqtt.connect({
port: 8883,
host: "localhost",
username: "user1",
password: "XXXXX",
key: fs.readFileSync(path.join(__dirname, './certs/client.key')),
cert: fs.readFileSync(path.join(__dirname, './certs/client.crt')),
rejectUnauthorized: false,
ca: fs.readFileSync(path.join(__dirname, './certs/localhost.crt')),
protocol: 'mqtts'
}

);

console.log("connecting");
client.on("connect", () => {
console.log("connected");
client.subscribe("topic/2");
});

client.on("message", (topic, payload) => {
console.log([topic, new TextDecoder().decode(payload)]);
});

setInterval(async () => {
await client.publish("topic/1", "from publisher1");
}, 2000);
1
$ node client-mqttjs.js

これで、mqtts で通信できることが確認できた。
この後 Deno 向け移植を再度試てみたものの、上手く移植はできなかった。

まとめ

  • MQTT は、Deno での動作可能
  • MQTTS は、(調べた限り) Deno ではまだいけなさそう
    • MQTT.js x Node.js 環境では動作確認できた。
  • MQTTS のセットアップを docker 上の mosquitto と 自己署名証明書 でやるのはなかなか辛そう
    • ローカルマシンに mosquitto のセットアップと 自己署名証明書 は動作確認できた

MQTTを Deno で動かすことにトライしてみた。
MQTTSを動作できそうになかったのが少し残念ではあったが、ひとしきり調べたいことは確認できたので良しとしたい。
また定期的に調べることにしておきたい。
(Deno に互換性の内容で issue を出しておいた。どうなるやら。)

では。