diff --git a/iot/http_example/README.md b/iot/http_example/README.md index 923fd61140..f17ea10929 100644 --- a/iot/http_example/README.md +++ b/iot/http_example/README.md @@ -25,18 +25,18 @@ Example Google Cloud IoT Core HTTP device connection code. Options: - -h, --help output usage information - --project_id GCP cloud project name. - --registry_id Cloud IoT Core registry id. - --device_id Cloud IoT Core device id. - --private_key_file Path to private key file. - --algorithm Encryption algorithm to generate the JWT. - Either RS256 (RSA) or ES256 (Eliptic Curve) - --cloud_region [region] GCP cloud region - --num_messages [num] Number of messages to publish. - --token_exp_mins [num] Minutes to JWT token expiration. - --http_bridge_address [address] HTTP bridge address. - --message_type [events|state] The message type to publish. + -h, --help output usage information + --projectId GCP cloud project name. + --registryId Cloud IoT Core registry id. + --deviceId Cloud IoT Core device id. + --privateKeyFile Path to private key file. + --algorithm Encryption algorithm to generate the JWT. + Either RS256 (RSA) or ES256 (Eliptic Curve) + --cloudRegion [region] GCP cloud region + --numMessages [num] Number of messages to publish. + --tokenExpMins [num] Minutes to JWT token expiration. + --httpBridgeAddress [address] HTTP bridge address. + --messageType [events|state] The message type to publish. For example, if your project ID is `blue-jet-123`, your service account credentials are stored in your home folder in creds.json and you have generated @@ -44,10 +44,10 @@ your credentials using the shell script provided in the parent folder, you can run the sample as: node cloudiot_http_example_nodejs.js \ - --project_id=blue-jet-123 \ - --registry_id=my-registry \ - --device_id=my-node-device \ - --private_key_file=../rsa_private.pem \ + --projectId=blue-jet-123 \ + --registryId=my-registry \ + --deviceId=my-node-device \ + --privateKeyFile=../rsa_private.pem \ --algorithm=RS256 # Reading Cloud Pub/Sub messages written by the sample client diff --git a/iot/http_example/cloudiot_http_example_nodejs.js b/iot/http_example/cloudiot_http_example.js similarity index 71% rename from iot/http_example/cloudiot_http_example_nodejs.js rename to iot/http_example/cloudiot_http_example.js index 50a88b4288..9b84c123dd 100644 --- a/iot/http_example/cloudiot_http_example_nodejs.js +++ b/iot/http_example/cloudiot_http_example.js @@ -23,31 +23,31 @@ const request = require('request'); console.log('Google Cloud IoT Core HTTP example.'); var argv = require(`yargs`) .options({ - project_id: { + projectId: { default: process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT, description: 'The Project ID to use. Defaults to the value of the GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT environment variables.', requiresArg: true, type: 'string' }, - cloud_region: { + cloudRegion: { default: 'us-central1', description: 'GCP cloud region.', requiresArg: true, type: 'string' }, - registry_id: { + registryId: { description: 'Cloud IoT registry ID.', requiresArg: true, demandOption: true, type: 'string' }, - device_id: { + deviceId: { description: 'Cloud IoT device ID.', requiresArg: true, demandOption: true, type: 'string' }, - private_key_file: { + privateKeyFile: { description: 'Path to private key file.', requiresArg: true, demandOption: true, @@ -60,25 +60,25 @@ var argv = require(`yargs`) choices: ['RS256', 'ES256'], type: 'string' }, - num_messages: { + numMessages: { default: 100, description: 'Number of messages to publish.', requiresArg: true, type: 'number' }, - token_exp_mins: { + tokenExpMins: { default: 20, description: 'Minutes to JWT token expiration.', requiresArg: true, type: 'number' }, - http_bridge_address: { + httpBridgeAddress: { default: 'cloudiot-device.googleapis.com', description: 'HTTP bridge address.', requiresArg: true, type: 'string' }, - message_type: { + messageType: { default: 'events', description: 'Message type to publish.', requiresArg: true, @@ -86,7 +86,7 @@ var argv = require(`yargs`) type: 'string' } }) - .example(`node $0 cloudiot_http_example_nodejs.js --project_id=blue-jet-123 --registry_id=my-registry --device_id=my-node-device --private_key_file=../rsa_private.pem --algorithm=RS256`) + .example(`node $0 cloudiotHttp_example_nodejs.js --projectId=blue-jet-123 --registryId=my-registry --deviceId=my-node-device --privateKeyFile=../rsaPrivate.pem --algorithm=RS256`) .wrap(120) .recommendCommands() .epilogue(`For more information, see https://cloud.google.com/iot-core/docs`) @@ -94,6 +94,21 @@ var argv = require(`yargs`) .strict() .argv; +// [START iot_http_variables] +// A unique string that identifies this device. For Google Cloud IoT Core, it +// must be in the format below. + +let iatTime = parseInt(Date.now() / 1000); +let authToken = createJwt(argv.projectId, argv.privateKeyFile, argv.algorithm); +const devicePath = `projects/${argv.projectId}/locations/${argv.cloudRegion}/registries/${argv.registryId}/devices/${argv.deviceId}`; + +// The request path, set accordingly depending on the message type. +const pathSuffix = argv.messageType === 'events' + ? ':publishEvent' : ':setState'; +const urlBase = `https://${argv.httpBridgeAddress}/v1beta1/${devicePath}`; +const url = `${urlBase}${pathSuffix}`; +// [END iot_http_variables] + // Create a Cloud IoT Core JWT for the given project ID, signed with the given // private key. // [START iot_http_jwt] @@ -115,11 +130,11 @@ function createJwt (projectId, privateKeyFile, algorithm) { // messageCount. Telemetry events are published at a rate of 1 per second and // states at a rate of 1 every 2 seconds. // [START iot_http_publish] -function publishAsync (messageCount, numMessages) { - const payload = `${argv.registry_id}/${argv.device_id}-payload-${messageCount}`; +function publishAsync (authToken, messageCount, numMessages) { + const payload = `${argv.registryId}/${argv.deviceId}-payload-${messageCount}`; console.log('Publishing message:', payload); const binaryData = Buffer.from(payload).toString('base64'); - const postData = argv.message_type === 'events' ? { + const postData = argv.messageType === 'events' ? { binary_data: binaryData } : { state: { @@ -129,16 +144,15 @@ function publishAsync (messageCount, numMessages) { const options = { url: url, headers: { - 'authorization': 'Bearer ' + authToken, + 'authorization': `Bearer ${authToken}`, 'content-type': 'application/json', 'cache-control': 'no-cache' - }, json: true, body: postData }; // Send events for high-frequency updates, update state only occasionally. - const delayMs = argv.message_type === 'events' ? 1000 : 2000; + const delayMs = argv.messageType === 'events' ? 1000 : 2000; request.post(options, function (error, response, body) { if (error) { console.error('Received error: ', error); @@ -152,11 +166,10 @@ function publishAsync (messageCount, numMessages) { // messageCount + 1. setTimeout(function () { let secsFromIssue = parseInt(Date.now() / 1000) - iatTime; - if (secsFromIssue > argv.token_exp_mins * 60) { + if (secsFromIssue > argv.tokenExpMins * 60) { iatTime = parseInt(Date.now() / 1000); console.log(`\tRefreshing token after ${secsFromIssue} seconds.`); - - authToken = createJwt(argv.project_id, argv.private_key_file, argv.algorithm); + authToken = createJwt(argv.projectId, argv.privateKeyFile, argv.algorithm); } publishAsync(messageCount + 1, numMessages); @@ -166,18 +179,36 @@ function publishAsync (messageCount, numMessages) { } // [END iot_http_publish] +// [START iot_http_getconfig] +function getConfig (authToken, version) { + console.log(`Getting config from URL: ${urlBase}`); + const options = { + url: urlBase + '/config?local_version=' + version, + headers: { + 'authorization': `Bearer ${authToken}`, + 'content-type': 'application/json', + 'cache-control': 'no-cache' + + }, + json: true + }; + request.get(options, function (error, response, body) { + if (error) { + console.error('Received error: ', error); + } else if (response.body.error) { + console.error(`Received error: ${JSON.stringify(response.body.error)}`); + } else { + console.log('Received config', JSON.stringify(body)); + } + }); +} +// [END iot_http_getconfig] + // [START iot_run_http] -// A unique string that identifies this device. For Google Cloud IoT Core, it -// must be in the format below. -const devicePath = `projects/${argv.project_id}/locations/${argv.cloud_region}/registries/${argv.registry_id}/devices/${argv.device_id}`; -// The request path, set accordingly depending on the message type. -const pathSuffix = argv.message_type === 'events' - ? ':publishEvent' : ':setState'; -const url = `https://${argv.http_bridge_address}/v1beta1/${devicePath}${pathSuffix}`; -let iatTime = parseInt(Date.now() / 1000); -let authToken = createJwt(argv.project_id, argv.private_key_file, argv.algorithm); +// Print latest configuration +getConfig(authToken, 0); // Publish messages. -publishAsync(1, argv.num_messages); +publishAsync(authToken, 1, argv.numMessages); // [END iot_run_http] diff --git a/iot/http_example/package.json b/iot/http_example/package.json index 89c6b77985..a1aab57597 100644 --- a/iot/http_example/package.json +++ b/iot/http_example/package.json @@ -5,10 +5,21 @@ "license": "Apache-2.0", "author": "Google Inc.", "main": "cloudiot_http_example_nodejs.js", + "scripts": { + "lint": "samples lint", + "pretest": "npm run lint", + "test": "samples test run --cmd ava -- -T 3m --verbose system-test/*.test.js" + }, "dependencies": { + "@google-cloud/pubsub": "0.13.2", + "@google-cloud/nodejs-repo-tools": "1.4.17", + "ava": "0.22.0", "yargs": "8.0.2", "jsonwebtoken": "7.4.1", - "request": "2.82.0" + "request": "2.82.0", + "uuid": "3.1.0" + }, + "testDependencies": { }, "devDependencies": {} } diff --git a/iot/http_example/resources/README.md b/iot/http_example/resources/README.md new file mode 100644 index 0000000000..ba272ccaf7 --- /dev/null +++ b/iot/http_example/resources/README.md @@ -0,0 +1,4 @@ +# Test public certificate files + +The public certificates in this folder are only provided for testing and should +not be used for registering your devices. diff --git a/iot/http_example/resources/rsa_cert.pem b/iot/http_example/resources/rsa_cert.pem new file mode 100644 index 0000000000..d81ae7c4f4 --- /dev/null +++ b/iot/http_example/resources/rsa_cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9TCCAd2gAwIBAgIJALM44e3ivEWkMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV +BAMMBnVudXNlZDAeFw0xNzEyMDcwMDQ1MjdaFw0yNzEyMDUwMDQ1MjdaMBExDzAN +BgNVBAMMBnVudXNlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL4w +BxHuEyYdbiwKiD8yXY7vYpcygeOQ4/ZdEg3wCB2OuYcaFRcCuqLcTLMnuzdcL+y3 +HBjWkrRW658cg3NG93Vj0iwSrga6u24CGBNYV+h8MBvwaDxk+uubnd5M/Q2OyL1J +GiMxQ1blR/71Hr5hhqaQZ2+qOF6kuf1m9rLUtMUJwOKp/PjPDmy654ZGsFWFSZmy +eRpNzmGU+KJg0o+Qf+sm75a8gQZ8AsrqveW0S/8o+zAjD0SkPcd01QBmYzQhjbi/ +LGGITrzbaB3ld9umJBIcXfnYPYisJfwSsT/jFwiXhrhpxNNaIaKlTzlQIt5l8bSs +HXzJBbuIg5Jb/SyIEpkCAwEAAaNQME4wHQYDVR0OBBYEFOfaQTUVAoNb6fc7qzzl +uKyHGrCYMB8GA1UdIwQYMBaAFOfaQTUVAoNb6fc7qzzluKyHGrCYMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBALKKDtiV1YV8k0YsNdiIXRlS3jsuoGuI +VVBrvDGz5Hel0rH9YmmfPS/Yn08kk3DF8Uynr4Xo1Zt9hmhgoq3ZoWm7MIP1+a9s +WyACyEMhVQSCzQrexRvG5ElpHx/vNjbcwiBkE5urlIvMBVt+BRRNKMNWq6F9ae63 +FxRp7CtNFSbibtLJuPgCs6qoNs0nlt2FPsNvs7jpPipj69o+egVckvQjAyppirWO ++jO5hCLy7EahLz2wCn90z0Xf9lhOZni9meaV1Vy3CHHg6jwIB8/XlRaHFrOGMGXg +h8eQqsmpk9/3o8pv00yj6Hkq+swVg7Rg9FZaUiOv/HO/J7stWU7qPbI= +-----END CERTIFICATE----- diff --git a/iot/http_example/resources/rsa_private.pem b/iot/http_example/resources/rsa_private.pem new file mode 100644 index 0000000000..06a66e3d40 --- /dev/null +++ b/iot/http_example/resources/rsa_private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+MAcR7hMmHW4s +Cog/Ml2O72KXMoHjkOP2XRIN8AgdjrmHGhUXArqi3EyzJ7s3XC/stxwY1pK0Vuuf +HINzRvd1Y9IsEq4GurtuAhgTWFfofDAb8Gg8ZPrrm53eTP0Njsi9SRojMUNW5Uf+ +9R6+YYamkGdvqjhepLn9Zvay1LTFCcDiqfz4zw5suueGRrBVhUmZsnkaTc5hlPii +YNKPkH/rJu+WvIEGfALK6r3ltEv/KPswIw9EpD3HdNUAZmM0IY24vyxhiE6822gd +5XfbpiQSHF352D2IrCX8ErE/4xcIl4a4acTTWiGipU85UCLeZfG0rB18yQW7iIOS +W/0siBKZAgMBAAECggEAfwLmBdRfl2m6JNFX0hSZpJY72kuRsN8XTnUzVHmDgfHJ +9u61POvGpnLHCjIzdjIrk0NqETBjQup1aooJQ1gWdKAYQPSsobPc7geZ+nlaI9mj +61Su1/58EBKZ6Faz/HTpnHeQbAY/OW3fmeYrBOtumBgB6/HauWH7D77Oa/lfS+Ij +4f6OVAxevsi6PUtNmNtBwk5S0lZl9SFcKeHurVindquX9vWZjBEEFtNXazJttIJS +z9KX29VYwoLfflIKaUKckn8X+wYc+3u3BvH8zJpd60yQ6MSo7Sb1XkxT9549m+JW +Cb+i1K7MC/yQo4mvDtAQIVBh8p8qpd4VjpBwMuUbgQKBgQDexuAaLO3adSYFXGwW +nom6Mz/ImYcpxYo0ouAR1talbmF5/oKl9Tcwh7l1eDHfe70gfeP+g4uwAcc1hx3a +ZtXusrJFBktFezlFQnZXaE5ppryrFWeu0he0RYLAVxnL6IlP9dYQhVsTZm+7uX5d +UP7aZtmOU9ZTEsAoqvjJQXvaCQKBgQDajPebXOxIUj8ffGTeiPZczTwXux04caDC +eFKSCbAlHWgG7mR4P3fQONfEGWNHF0CxBSrew9CHmKdPyiISaExCfUaUWDDCPQCp +UE5VAHPdjSlb4lqi+cyNVlJxBJGONtyYkbQNd6N9GHMnBS8jZi7zf8VzIXpeExA4 +n79Aml/YEQKBgDFrGId19AWD+z0xNWEHJjJB8CJFvHANvAzVHLOYXuEvzTvMs5qw +/N8tHHzsftO+lUPB6XOqJrCSlGhRYtPx//8FcPpS3Ru6rAerKKlXIB3buPqSsv9a +55s72DdmmvhayysLs8LSclOpY5vXGCsHLqGwMw6Zlm+zNyFOXAX5GspRAoGAaJMx +W68ABK8OM0OzhGQm9kriKTzIg5yjXspyQBzQo0HJ6B8kBgHgk8rPO68mOPsgYlPl +qogp/OgHjv9ahFJRwzLslckJM7g628loYfYAew+zrZrG4dsDjNG0Sw3zlAgeUAbQ +D+2iVhZf61josFiRuMP3t9paEi+vAFk4C3KSz/ECgYBpi1akpIzsYehW5uOL7Jhw +Hay5eshQ4vmHYuhDnn3gtT3h6J7TMwWs9pOygBG1I1b7GJ+tp4BZWJ2PmI7P8s45 +jdI99WODHwv03lAzjLwigoqDUDduaYqXcGghcGht5Sknkl2uYDChwLtI5JdBZ9/x +8h9dE9oAiH/KTzhPmK1E1Q== +-----END PRIVATE KEY----- diff --git a/iot/http_example/system-test/cloudiot_http_example_test.js b/iot/http_example/system-test/cloudiot_http_example_test.js new file mode 100644 index 0000000000..c689e8c06e --- /dev/null +++ b/iot/http_example/system-test/cloudiot_http_example_test.js @@ -0,0 +1,108 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const path = require(`path`); +const PubSub = require(`@google-cloud/pubsub`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); +const uuid = require(`uuid`); + +const deviceId = `test-node-device`; +const topicName = `nodejs-docs-samples-test-iot-${uuid.v4()}`; +const registryName = `nodejs-test-registry-iot-${uuid.v4()}`; +const helper = `node ../manager/manager.js`; +const cmd = `node cloudiot_http_example.js --registryId="${registryName}" --deviceId="${deviceId}" `; +const cwd = path.join(__dirname, `..`); + +test.before(tools.checkCredentials); +test.before(async () => { + const pubsub = PubSub(); + return pubsub.createTopic(topicName) + .then((results) => { + const topic = results[0]; + console.log(`Topic ${topic.name} created.`); + return topic; + }); +}); + +test.after.always(async () => { + const pubsub = PubSub(); + const topic = pubsub.topic(topicName); + return topic.delete() + .then(() => { + console.log(`Topic ${topic.name} deleted.`); + }); +}); + +test(`should receive configuration message`, async (t) => { + const localDevice = `test-rsa-device`; + const localRegName = `${registryName}-rsa256`; + await tools.runAsync(`${helper} setupIotTopic ${topicName}`, cwd); + await tools.runAsync( + `${helper} createRegistry ${localRegName} ${topicName}`, cwd); + await tools.runAsync( + `${helper} createRsa256Device ${localDevice} ${localRegName} resources/rsa_cert.pem`, cwd); + + const output = await tools.runAsync( + `${cmd} --messageType=events --numMessages=1 --privateKeyFile=resources/rsa_private.pem --algorithm=RS256`, cwd); + + t.regex(output, new RegExp(/Getting config/)); + + // Check / cleanup + await tools.runAsync(`${helper} getDeviceState ${localDevice} ${localRegName}`, cwd); + await tools.runAsync(`${helper} deleteDevice ${localDevice} ${localRegName}`, cwd); + await tools.runAsync(`${helper} deleteRegistry ${localRegName}`, cwd); +}); + +test(`should send event message`, async (t) => { + const localDevice = `test-rsa-device`; + const localRegName = `${registryName}-rsa256`; + await tools.runAsync(`${helper} setupIotTopic ${topicName}`, cwd); + await tools.runAsync( + `${helper} createRegistry ${localRegName} ${topicName}`, cwd); + await tools.runAsync( + `${helper} createRsa256Device ${localDevice} ${localRegName} resources/rsa_cert.pem`, cwd); + + const output = await tools.runAsync( + `${cmd} --messageType=events --numMessages=1 --privateKeyFile=resources/rsa_private.pem --algorithm=RS256`, cwd); + + t.regex(output, new RegExp(/Publishing message/)); + + // Check / cleanup + await tools.runAsync(`${helper} getDeviceState ${localDevice} ${localRegName}`, cwd); + await tools.runAsync(`${helper} deleteDevice ${localDevice} ${localRegName}`, cwd); + await tools.runAsync(`${helper} deleteRegistry ${localRegName}`, cwd); +}); + +test(`should send event message`, async (t) => { + const localDevice = `test-rsa-device`; + const localRegName = `${registryName}-rsa256`; + await tools.runAsync(`${helper} setupIotTopic ${topicName}`, cwd); + await tools.runAsync( + `${helper} createRegistry ${localRegName} ${topicName}`, cwd); + await tools.runAsync( + `${helper} createRsa256Device ${localDevice} ${localRegName} resources/rsa_cert.pem`, cwd); + + const output = await tools.runAsync( + `${cmd} --messageType=state --numMessages=1 --privateKeyFile=resources/rsa_private.pem --algorithm=RS256`, cwd); + t.regex(output, new RegExp(/Publishing message/)); + + // Check / cleanup + await tools.runAsync(`${helper} getDeviceState ${localDevice} ${localRegName}`, cwd); + await tools.runAsync(`${helper} deleteDevice ${localDevice} ${localRegName}`, cwd); + await tools.runAsync(`${helper} deleteRegistry ${localRegName}`, cwd); +});