この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
先日のアップデートで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のエンドポイントって本当に凄いです。
第一報を聞いた時にはコーヒー吹き出しました。というわけでガンガン使って行きましょう、ではまた!