AWS IoTのMQTT over WebSocketにHTMLから接続してみた
はじめに
先日のアップデートでAWS IoT Device GatewayがWebSocketに対応しました。
このアップデートにより、JavaScriptで動作するMQTTクライアントを用いてWebページ上からAWS IoT Device GatewayにPub/Subすることが可能になりました。(MQTT over WebSockets)
今回はローカルのHTMLをIoT Device Gatewayと連携させる方法をご紹介します。
使ってみる
Pub/Sub可能なIAMユーザーの作成
ブラウザからMQTT over WebSocketsを使うには、今のところPub/Sub権限を持ったIAMユーザーのクレデンシャル(アクセスキーとシークレットキー)が必要です。
公開したときに悪用されないために、最低限の権限を持ったIAMユーザーを作成しましょう。
今回はweb-chat-iotというユーザーを作成しました。
作成したユーザーにはAWSIoTDataAccessポリシーのみをアタッチします。
今回はテストなので管理ポリシーをアタッチしていますが、もし実際に本番で使うような事がある場合は、インラインポリシーでより限定的なカスタムポリシーを定義してください。
コード(HTML)の前提
MQTTクライアントとしてPaho JavaScript Clientを使います。
今回利用するライブラリは下記のとおりです。
名称 | 利用用途 |
---|---|
Crypto-JS | 署名バージョン4の生成で使用 |
Moment.js | 署名バージョン4の生成で使用 |
Vue.js | サンプル画面のレンダリング |
Paho JavaScript Client | ブラウザ上で動作するMQTTクライアント |
上記のうち、Paho JavaScript ClientについてはCDNによる提供がありません。
Paho JavaScript Clientの Download から mqttws31.js をダウンロードしておいてください。
サンプルコード
<html lang="ja"> <body> <ul id="chat"> <li v-for="m in messages">{{ m }}</li> </ul> <input type="text" name="say" id="say" placeholder="Input a message here..."> <button id="send">Send</button> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.16/vue.min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/core-min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/hmac-min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/sha256-min.js" type="text/javascript"></script> <script src="./mqttws31.js" type="text/javascript"></script> <script type="text/javascript"> var data = { messages: [] }; new Vue({ el: '#chat', data: data }); document.getElementById('send').addEventListener('click', function (e) { var say = document.getElementById('say') send(say.value); say.value = ''; }); function SigV4Utils(){} SigV4Utils.sign = function(key, msg) { var hash = CryptoJS.HmacSHA256(msg, key); return hash.toString(CryptoJS.enc.Hex); }; SigV4Utils.sha256 = function(msg) { var hash = CryptoJS.SHA256(msg); return hash.toString(CryptoJS.enc.Hex); }; SigV4Utils.getSignatureKey = function(key, dateStamp, regionName, serviceName) { var kDate = CryptoJS.HmacSHA256(dateStamp, 'AWS4' + key); var kRegion = CryptoJS.HmacSHA256(regionName, kDate); var kService = CryptoJS.HmacSHA256(serviceName, kRegion); var kSigning = CryptoJS.HmacSHA256('aws4_request', kService); return kSigning; }; function createEndpoint(regionName, awsIotEndpoint, accessKey, secretKey) { var time = moment.utc(); var dateStamp = time.format('YYYYMMDD'); var amzdate = dateStamp + 'T' + time.format('HHmmss') + 'Z'; var service = 'iotdevicegateway'; var region = regionName; var secretKey = secretKey; var accessKey = accessKey; var algorithm = 'AWS4-HMAC-SHA256'; var method = 'GET'; var canonicalUri = '/mqtt'; var host = awsIotEndpoint; var credentialScope = dateStamp + '/' + region + '/' + service + '/' + 'aws4_request'; var canonicalQuerystring = 'X-Amz-Algorithm=AWS4-HMAC-SHA256'; canonicalQuerystring += '&X-Amz-Credential=' + encodeURIComponent(accessKey + '/' + credentialScope); canonicalQuerystring += '&X-Amz-Date=' + amzdate; canonicalQuerystring += '&X-Amz-SignedHeaders=host'; var canonicalHeaders = 'host:' + host + '\n'; var payloadHash = SigV4Utils.sha256(''); var canonicalRequest = method + '\n' + canonicalUri + '\n' + canonicalQuerystring + '\n' + canonicalHeaders + '\nhost\n' + payloadHash; var stringToSign = algorithm + '\n' + amzdate + '\n' + credentialScope + '\n' + SigV4Utils.sha256(canonicalRequest); var signingKey = SigV4Utils.getSignatureKey(secretKey, dateStamp, region, service); var signature = SigV4Utils.sign(signingKey, stringToSign); canonicalQuerystring += '&X-Amz-Signature=' + signature; return 'wss://' + host + canonicalUri + '?' + canonicalQuerystring; } var endpoint = createEndpoint( 'ap-northeast-1', // Your Region 'yourendpoint.iot.ap-northeast-1.amazonaws.com', // Require 'lowercamelcase'!! 'YOUR_AWS_ACCESS_KEY', 'YOUR_AWS_SECRET_ACCESS_KEY'); var clientId = Math.random().toString(36).substring(7); var client = new Paho.MQTT.Client(endpoint, clientId); var connectOptions = { useSSL: true, timeout: 3, mqttVersion: 4, onSuccess: subscribe }; client.connect(connectOptions); client.onMessageArrived = onMessage; client.onConnectionLost = function(e) { console.log(e) }; function subscribe() { client.subscribe("Test/chat"); console.log("subscribed"); } function send(content) { var message = new Paho.MQTT.Message(content); message.destinationName = "Test/chat"; client.send(message); console.log("sent"); } function onMessage(message) { data.messages.push(message.payloadString); console.log("message received: " + message.payloadString); } </script> </body> </html>
動作画面
解説
AWSのドキュメントがとても詳細に記述されているので、それに従えばOKです。
手順としては
- WebSocketコネクション要求時に発生するHTTPリクエストのモックを作成
- リクエストのモックを元に署名バージョン4のQueryStringを作成
- AWS IoT Device GatewayのMQTT over WebSocketsエンドポイントに上記のQueryStringを付加してWSS接続
という感じです。
また、チャット部分の実装は最小限のものなのでPahoのドキュメントを参考に読んでいただければと思います。
さて、基本的にはAWSドキュメント通りに記述すればOKなのですが、一点ハマるかもしれないポイントがあります。
署名バージョン4の作成時ですが、HTTPリクエストのモックを作成するために host
として AWS IoTのエンドポイント を渡してやる必要があります。
このAWS IoTのエンドポイントは、aws-cliのiot describe-endpointコマンドで取得可能です。
$ aws iot describe-endpoint { "endpointAddress": "YOURENDPOINTID.iot.ap-northeast-1.amazonaws.com" }
上記のように、大文字小文字混じりでエンドポイントが返されます。
さて、このエンドポイントをそのままモックHTTPリクエスト作成に使用すると、正しい署名バージョン4が作成されません。
理由は、実際のHTTPリクエストの host
はすべて小文字に変換された yourendpoint.iot.ap-northeast-1.amazonaws.com
になるためです。
そのため host
はlowercaseで渡してやる必要があります。
一応これはAWSの署名バージョン4に関するドキュメントには記述されているのですが
8. Create a digest (hash) of the canonical request with the same algorithm that you used to hash the payload.
というように、"ペイロードをハッシュした時と同じアルゴリズムで生成して下さい"という表現になっているため(そのアルゴリズムに小文字への変換が含まれている)見落としてしまいがちかもしれません。ご注意を!
まとめ
- 署名バージョン4の「正規リクエストのハッシュ」を生成する際には、ホスト名がlowercaseになっていることを確認します。
- ブラウザ上で動くMQTTのクライアントがAWS IotとPub/Subするために必要な権限は以下の通りです。
- iot:Connect
- iot:Publish
- iot:Subscribe
- iot:Receive
- 2015/02現在はNode.jsとiOSのみIoT Data用のSDKが用意されています。
- アプリケーションサーバなどからPublishしたい場合には、Node.jsで実装したAWS Lambda Functionを介すと楽ちんです。
- もちろん各言語用のMQTTクライアントと証明書を用いて、自前で接続してPublishしたりもできます。
スケールするマネージドなWeb Socket Secureのエンドポイントって本当に凄いです。
第一報を聞いた時にはコーヒー吹き出しました。というわけでガンガン使って行きましょう、ではまた!