diff --git a/iot/api-client/README.md b/iot/api-client/README.md new file mode 100644 index 00000000000..2c61fe001a9 --- /dev/null +++ b/iot/api-client/README.md @@ -0,0 +1,38 @@ +# Cloud IoT Core Java Samples +This folder contains Java samples that demonstrate an overview of the +Google Cloud IoT Core platform. + +## Quickstart + +1. Install the gCloud CLI as described in [the device manager guide](https://cloud-dot-devsite.googleplex.com/iot/docs/device_manager_guide). +2. Create a PubSub topic: + + gcloud beta pubsub topics create projects/my-iot-project/topics/device-events + +3. Add the special account `cloud-iot@system.gserviceaccount.com` to that +PubSub topic from the [Cloud Developer Console](https://console.cloud.google.com) +or by using the helper script in the [/scripts](./scripts) folder. + +4. Create a registry: + + gcloud alpha iot registries create my-registry \ + --project=my-iot-project \ + --region=us-central1 \ + --pubsub-topic=projects/my-iot-project/topics/device-events + +5. Use the [`generate_keys.sh`](generate_keys.sh) script to generate your signing keys: + + ./generate_keys.sh + +6. Create a device. + + gcloud alpha iot devices create my-java-device \ + --project=my-iot-project \ + --region=us-central1 \ + --registry=my-registry \ + --public-key path=rsa_cert.pem,type=rs256 + +7. Connect a sample device using the sample app in the [`mqtt_example`](./mqtt_example) folder. +8. Learn how to manage devices programatically with the sample app in the +`manager` folder. + diff --git a/iot/api-client/generate_keys.sh b/iot/api-client/generate_keys.sh new file mode 100755 index 00000000000..e9beedc3dd6 --- /dev/null +++ b/iot/api-client/generate_keys.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# 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. + +openssl req -x509 -newkey rsa:2048 -keyout rsa_private.pem -nodes -out \ + rsa_cert.pem -subj "/CN=unused" +openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem +openssl ec -in ec_private.pem -pubout -out ec_public.pem +openssl pkcs8 -topk8 -inform PEM -outform DER -in rsa_private.pem \ + -nocrypt > rsa_private_pkcs8 +openssl pkcs8 -topk8 -inform PEM -outform DER -in ec_private.pem \ + -nocrypt > ec_private_pkcs8 diff --git a/iot/api-client/manager/README.md b/iot/api-client/manager/README.md new file mode 100644 index 00000000000..c2036a5277c --- /dev/null +++ b/iot/api-client/manager/README.md @@ -0,0 +1,43 @@ +# Cloud IoT Core Java Device Management example + +This sample app demonstrates device management for Google Cloud IoT Core. + +Note that before you can run the sample, you must configure a Google Cloud +PubSub topic for Cloud IoT as described in [the parent README](../README.md). + +## Setup + +Manually install [the provided client library](https://cloud.google.com/iot/resources/java/cloud-iot-core-library.jar) +for Cloud IoT Core to Maven: + + mvn install:install-file -Dfile=cloud-iot-core-library.jar -DgroupId=com.google.apis \ + -DartifactId=google-api-services-cloudiot -Dversion=v1beta1-rev20170418-1.22.0-SNAPSHOT \ + -Dpackaging=jar + +Run the following command to install the libraries and build the sample with +Maven: + +mvn clean compile assembly:single + +## Running the sample + +The following command summarizes the sample usage: + + mvn exec:java \ + -Dexec.mainClass="com.google.cloud.iot.examples.DeviceRegistryExample" \ + -Dexec.args="-project_id=my-project-id \ + -pubsub_topic=projects/my-project-id/topics/my-topic-id \ + -ec_public_key_file=/path/to/ec_public.pem \ + -rsa_certificate_file=/path/to/rsa_cert.pem" + +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 +your credentials using the shell script provided in the parent folder, you can +run the sample as: + + mvn exec:java \ + -Dexec.mainClass="com.google.cloud.iot.examples.DeviceRegistryExample" \ + -Dexec.args="-project_id=blue-jet-123 \ + -pubsub_topic=projects/blue-jet-123/topics/device-events \ + -ec_public_key_file=../ec_public.pem \ + -rsa_certificate_file=../rsa_cert.pem" diff --git a/iot/api-client/manager/pom.xml b/iot/api-client/manager/pom.xml new file mode 100644 index 00000000000..100af9a82e2 --- /dev/null +++ b/iot/api-client/manager/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + com.example.cloud + cloudiot-manager-demo + jar + 1.0 + cloudiot-manager-demo + http://maven.apache.org + + + + doc-samples + com.google.cloud + 1.0.0 + ../../../ + + + + 1.7 + 1.7 + + + + + com.google.apis + google-api-services-cloudiot + v1beta1-rev20170418-1.22.0-SNAPSHOT + + + com.google.oauth-client + google-oauth-client + 1.22.0 + + + com.google.api-client + google-api-client + 1.22.0 + + + commons-cli + commons-cli + 1.3 + + + + + + + maven-assembly-plugin + + + + com.example.cloudiot.Manage + + + + jar-with-dependencies + + + + + + diff --git a/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/DeviceRegistryExample.java b/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/DeviceRegistryExample.java new file mode 100644 index 00000000000..e17766ea578 --- /dev/null +++ b/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/DeviceRegistryExample.java @@ -0,0 +1,334 @@ +/** + * 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. + */ +package com.google.cloud.iot.examples; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.Charsets; +import com.google.api.services.cloudiot.v1beta1.CloudIot; +import com.google.api.services.cloudiot.v1beta1.CloudIotScopes; +import com.google.api.services.cloudiot.v1beta1.model.Device; +import com.google.api.services.cloudiot.v1beta1.model.DeviceConfig; +import com.google.api.services.cloudiot.v1beta1.model.DeviceConfigData; +import com.google.api.services.cloudiot.v1beta1.model.DeviceCredential; +import com.google.api.services.cloudiot.v1beta1.model.DeviceRegistry; +import com.google.api.services.cloudiot.v1beta1.model.ModifyCloudToDeviceConfigRequest; +import com.google.api.services.cloudiot.v1beta1.model.NotificationConfig; +import com.google.api.services.cloudiot.v1beta1.model.PublicKeyCredential; +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.xml.bind.DatatypeConverter; + +/** + * Example of using Cloud IoT device manager API to administer devices, registries and projects. + * + *

This example uses the Device Manager API to create, retrieve, disable, list and delete Cloud + * IoT devices and registries, using both RSA and eliptic curve keys for authentication. + * + *

To start, follow the instructions on the Developer Guide at cloud.google.com/iot to create a + * service_account.json file and Cloud Pub/Sub topic as discussed in the guide. You will then need + * to point to the service_account.json file as described in + * https://developers.google.com/identity/protocols/application-default-credentials#howtheywork + * + *

Before running the example, we have to create private and public keys, as described in + * cloud.google.com/iot. Since we are interacting with the device manager, we will only use the + * public keys. The private keys are used to sign JWTs to authenticate devices. See the + * MQTT client example + * for more information. + * + *

Finally, compile and run the example with: + * + *

+ * 
+ * $ mvn clean compile assembly:single
+ * $ mvn exec:java \
+ *       -Dexec.mainClass="com.google.cloud.iot.examples.DeviceRegistryExample" \
+ *       -Dexec.args="-project_id=my-project-id \
+ *                    -pubsub_topic=projects/my-project-id/topics/my-topic-id \
+ *                    -ec_public_key_file=/path/to/ec_public.pem \
+ *                    -rsa_certificate_file=/path/to/rsa_cert.pem"
+ * 
+ * 
+ */ +public class DeviceRegistryExample { + // Service for administering Cloud IoT Core devices, registries and projects. + private CloudIot service; + // Path to the project and location: "projects/my-project-id/locations/us-central1" + private String projectPath; + // Path to the registry: "projects/my-project-id/location/us-central1/registries/my-registry-id" + private String registryPath; + + /** Construct a new registry with the given name and pubsub topic inside the given project. */ + public DeviceRegistryExample( + String projectId, String location, String registryName, String pubsubTopicPath) + throws IOException, GeneralSecurityException { + GoogleCredential credential = + GoogleCredential.getApplicationDefault().createScoped(CloudIotScopes.all()); + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + HttpRequestInitializer init = new RetryHttpInitializerWrapper(credential); + service = new CloudIot(GoogleNetHttpTransport.newTrustedTransport(), jsonFactory, init); + projectPath = "projects/" + projectId + "/locations/" + location; + registryPath = projectPath + "/registries/" + registryName; + + NotificationConfig notificationConfig = new NotificationConfig(); + notificationConfig.setPubsubTopicName(pubsubTopicPath); + + DeviceRegistry registry = new DeviceRegistry(); + registry.setEventNotificationConfig(notificationConfig); + registry.setId(registryName); + service.projects().locations().registries().create(projectPath, registry).execute(); + } + + /** Delete this registry from Cloud IoT. */ + public void delete() throws IOException { + System.out.println("Deleting: " + registryPath); + service.projects().locations().registries().delete(registryPath).execute(); + } + + /** Print all of the devices in this registry to standard out. */ + public void listDevices() throws IOException { + List devices = + service + .projects() + .locations() + .registries() + .devices() + .list(registryPath) + .execute() + .getDevices(); + + if (devices != null) { + System.out.println("Found " + devices.size() + " devices"); + for (Device d : devices) { + System.out.println("Id: " + d.getId()); + if (d.getConfig() != null) { + // Note that this will show the device config in Base64 encoded format. + System.out.println("Config: " + d.getConfig().toPrettyString()); + } + System.out.println(); + } + } else { + System.out.println("Registry has no devices."); + } + } + + private DeviceCredential createDeviceCredential(String publicKeyFilePath, String keyFormat) + throws IOException { + PublicKeyCredential publicKeyCredential = new PublicKeyCredential(); + String key = Files.toString(new File(publicKeyFilePath), Charsets.UTF_8); + publicKeyCredential.setKey(key); + publicKeyCredential.setFormat(keyFormat); + + DeviceCredential credential = new DeviceCredential(); + credential.setPublicKey(publicKeyCredential); + return credential; + } + + /** + * Create a new device in this registry with the given id. The device will be authenticated with + * the given public key file. + */ + private void createDevice(String deviceId, List credentials) + throws IOException { + System.out.println("Creating device with id: " + deviceId); + Device device = new Device(); + device.setId(deviceId); + device.setCredentials(credentials); + Device createdDevice = + service + .projects() + .locations() + .registries() + .devices() + .create(registryPath, device) + .execute(); + + System.out.println("Created device: " + createdDevice.toPrettyString()); + } + + /** Create a device that is authenticated using RS256. */ + public void createDeviceWithRs256(String deviceId, String certificateFilePath) + throws IOException { + createDevice( + deviceId, Arrays.asList(createDeviceCredential(certificateFilePath, "RSA_X509_PEM"))); + } + + /** Create a device that is authenticated using ES256. */ + public void createDeviceWithEs256(String deviceId, String publicKeyFilePath) throws IOException { + createDevice(deviceId, Arrays.asList(createDeviceCredential(publicKeyFilePath, "ES256_PEM"))); + } + + /** + * Create a device that has no credentials. + * + *

This is a valid way to construct a device, however until it is patched with a credential the + * device will not be able to connect to Cloud IoT. + */ + public void createDeviceWithNoAuth(String deviceId) throws IOException { + createDevice(deviceId, new ArrayList()); + } + + /** Patch the device to add an ES256 key for authentication. */ + public void patchEs256ForAuth(String deviceId, String publicKeyFilePath) throws IOException { + String devicePath = registryPath + "/devices/" + deviceId; + PublicKeyCredential publicKeyCredential = new PublicKeyCredential(); + String key = Files.toString(new File(publicKeyFilePath), Charsets.UTF_8); + publicKeyCredential.setKey(key); + publicKeyCredential.setFormat("ES256_PEM"); + + DeviceCredential credential = new DeviceCredential(); + credential.setPublicKey(publicKeyCredential); + + Device device = new Device(); + device.setCredentials(Arrays.asList(credential)); + + Device patchedDevice = + service + .projects() + .locations() + .registries() + .devices() + .patch(devicePath, device) + .setFields("credentials") + .execute(); + + System.out.println("Patched device is " + patchedDevice.toPrettyString()); + } + + /** Delete the given device from the registry. */ + public void deleteDevice(String deviceId) throws IOException { + String devicePath = registryPath + "/devices/" + deviceId; + System.out.println("Deleting device " + devicePath); + service.projects().locations().registries().devices().delete(devicePath).execute(); + } + + /** List all of the configs for the given device. */ + public void listDeviceConfigs(String deviceId) throws IOException { + String devicePath = registryPath + "/devices/" + deviceId; + System.out.println("Listing device configs for " + devicePath); + List deviceConfigs = + service + .projects() + .locations() + .registries() + .devices() + .configVersions() + .list(devicePath) + .execute() + .getDeviceConfigs(); + + for (DeviceConfig config : deviceConfigs) { + System.out.println("Config version: " + config.getVersion()); + System.out.println("Contents: " + config.getData().getBinaryData()); + System.out.println(); + } + } + + /** Modify the latest cloud to device config for the given device, with the config data. */ + public void modifyCloudToDeviceConfig(String deviceId, String configData) throws IOException { + String devicePath = registryPath + "/devices/" + deviceId; + ModifyCloudToDeviceConfigRequest request = new ModifyCloudToDeviceConfigRequest(); + DeviceConfigData data = new DeviceConfigData(); + data.setBinaryData(DatatypeConverter.printBase64Binary(configData.getBytes(Charsets.UTF_8))); + request.setVersionToUpdate(0L); // 0L indicates update all versions + request.setData(data); + DeviceConfig config = + service + .projects() + .locations() + .registries() + .devices() + .modifyCloudToDeviceConfig(devicePath, request) + .execute(); + + System.out.println("Created device config: " + config.toPrettyString()); + } + + public static void main(String[] args) throws IOException, GeneralSecurityException { + DeviceRegistryExampleOptions options = DeviceRegistryExampleOptions.fromFlags(args); + if (options == null) { + // Could not parse. + return; + } + + // Simple example of interacting with the Cloud IoT API. + String registryName = "cloudiot_device_manager_example_registry_" + System.currentTimeMillis(); + + // Create a new registry with the above name. + DeviceRegistryExample registry = + new DeviceRegistryExample( + options.projectId, options.cloudRegion, registryName, options.pubsubTopic); + + // List the devices in the registry. Since we haven't created any yet, this should be empty. + registry.listDevices(); + + // Create a device that is authenticated using RSA. + String rs256deviceId = "rs256-device"; + registry.createDeviceWithRs256(rs256deviceId, options.rsaCertificateFile); + + // Create a device without an authentication credential. We'll patch it to use elliptic curve + // cryptography. + String es256deviceId = "es256-device"; + registry.createDeviceWithNoAuth(es256deviceId); + + // List the devices again. This should show the above two devices. + registry.listDevices(); + + // Give the device without an authentication credential an elliptic curve credential. + registry.patchEs256ForAuth(es256deviceId, options.ecPublicKeyFile); + + // List the devices in the registry again, still showing the two devices. + registry.listDevices(); + + // List the device configs for the RSA authenticated device. Since we haven't pushed any, this + // list will only contain the default empty config. + registry.listDeviceConfigs(rs256deviceId); + + // Push two new configs to the device. + registry.modifyCloudToDeviceConfig(rs256deviceId, "config v1"); + registry.modifyCloudToDeviceConfig(rs256deviceId, "config v2"); + + // List the configs again. This will show the two configs that we just pushed. + registry.listDeviceConfigs(rs256deviceId); + + // Delete the elliptic curve device. + registry.deleteDevice(es256deviceId); + + // Since we deleted the elliptic curve device, this will only show the RSA device. + registry.listDevices(); + + try { + // Try to delete the registry. However, since the registry is not empty, this will fail and + // throw an exception. + registry.delete(); + } catch (IOException e) { + System.out.println("Exception: " + e.getMessage()); + } + + // Delete the RSA device. The registry is now empty. + registry.deleteDevice(rs256deviceId); + + // Since the registry has no devices in it, the delete will succeed. + registry.delete(); + } +} diff --git a/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/DeviceRegistryExampleOptions.java b/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/DeviceRegistryExampleOptions.java new file mode 100644 index 00000000000..57e13153881 --- /dev/null +++ b/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/DeviceRegistryExampleOptions.java @@ -0,0 +1,100 @@ +/** + * 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. + */ +package com.google.cloud.iot.examples; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** Command line options for the Device Manager example. */ +public class DeviceRegistryExampleOptions { + String projectId; + String pubsubTopic; + String ecPublicKeyFile = "ec_public.pem"; + String rsaCertificateFile = "rsa_cert.pem"; + String cloudRegion = "us-central1"; + + /** Construct an DeviceRegistryExampleOptions class from command line flags. */ + public static DeviceRegistryExampleOptions fromFlags(String[] args) { + Options options = new Options(); + // Required arguments + options.addOption( + Option.builder() + .type(String.class) + .longOpt("project_id") + .hasArg() + .desc("GCP cloud project name.") + .required() + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("pubsub_topic") + .hasArg() + .desc( + "Pub/Sub topic to create registry with, i.e. 'projects/project-id/topics/topic-id'") + .required() + .build()); + + // Optional arguments. + options.addOption( + Option.builder() + .type(String.class) + .longOpt("ec_public_key_file") + .hasArg() + .desc("Path to ES256 public key file.") + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("rsa_certificate_file") + .hasArg() + .desc("Path to RS256 certificate file.") + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("cloud_region") + .hasArg() + .desc("GCP cloud region.") + .build()); + + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine; + try { + commandLine = parser.parse(options, args); + DeviceRegistryExampleOptions res = new DeviceRegistryExampleOptions(); + + res.projectId = commandLine.getOptionValue("project_id"); + res.pubsubTopic = commandLine.getOptionValue("pubsub_topic"); + + if (commandLine.hasOption("ec_public_key_file")) { + res.ecPublicKeyFile = commandLine.getOptionValue("ec_public_key_file"); + } + if (commandLine.hasOption("rsa_certificate_file")) { + res.rsaCertificateFile = commandLine.getOptionValue("rsa_certificate_file"); + } + if (commandLine.hasOption("cloud_region")) { + res.cloudRegion = commandLine.getOptionValue("cloud_region"); + } + return res; + } catch (ParseException e) { + System.err.println(e.getMessage()); + return null; + } + } +} diff --git a/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/RetryHttpInitializerWrapper.java b/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/RetryHttpInitializerWrapper.java new file mode 100644 index 00000000000..71aad88ef33 --- /dev/null +++ b/iot/api-client/manager/src/main/java/com/google/cloud/iot/examples/RetryHttpInitializerWrapper.java @@ -0,0 +1,101 @@ +/** + * 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. + */ +package com.google.cloud.iot.examples; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.http.HttpBackOffIOExceptionHandler; +import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpUnsuccessfulResponseHandler; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.client.util.Sleeper; +import com.google.common.base.Preconditions; + +import java.io.IOException; +import java.util.logging.Logger; + +/** + * RetryHttpInitializerWrapper will automatically retry upon RPC failures, preserving the + * auto-refresh behavior of the Google Credentials. + */ +public class RetryHttpInitializerWrapper implements HttpRequestInitializer { + + /** A private logger. */ + private static final Logger LOG = Logger.getLogger(RetryHttpInitializerWrapper.class.getName()); + + /** One minutes in milliseconds. */ + private static final int ONE_MINUTE_MILLIS = 60 * 1000; + + /** + * Intercepts the request for filling in the "Authorization" header field, as well as recovering + * from certain unsuccessful error codes wherein the Credential must refresh its token for a + * retry. + */ + private final Credential wrappedCredential; + + /** A sleeper; you can replace it with a mock in your test. */ + private final Sleeper sleeper; + + /** + * A constructor. + * + * @param wrappedCredential Credential which will be wrapped and used for providing auth header. + */ + public RetryHttpInitializerWrapper(final Credential wrappedCredential) { + this(wrappedCredential, Sleeper.DEFAULT); + } + + /** + * A protected constructor only for testing. + * + * @param wrappedCredential Credential which will be wrapped and used for providing auth header. + * @param sleeper Sleeper for easy testing. + */ + RetryHttpInitializerWrapper(final Credential wrappedCredential, final Sleeper sleeper) { + this.wrappedCredential = Preconditions.checkNotNull(wrappedCredential); + this.sleeper = sleeper; + } + + /** Initializes the given request. */ + @Override + public final void initialize(final HttpRequest request) { + request.setReadTimeout(2 * ONE_MINUTE_MILLIS); // 2 minutes read timeout + final HttpUnsuccessfulResponseHandler backoffHandler = + new HttpBackOffUnsuccessfulResponseHandler(new ExponentialBackOff()).setSleeper(sleeper); + request.setInterceptor(wrappedCredential); + request.setUnsuccessfulResponseHandler( + new HttpUnsuccessfulResponseHandler() { + @Override + public boolean handleResponse( + final HttpRequest request, final HttpResponse response, final boolean supportsRetry) + throws IOException { + if (wrappedCredential.handleResponse(request, response, supportsRetry)) { + // If credential decides it can handle it, the return code or message indicated + // something specific to authentication, and no backoff is desired. + return true; + } else if (backoffHandler.handleResponse(request, response, supportsRetry)) { + // Otherwise, we defer to the judgment of our internal backoff handler. + LOG.info("Retrying " + request.getUrl().toString()); + return true; + } else { + return false; + } + } + }); + request.setIOExceptionHandler( + new HttpBackOffIOExceptionHandler(new ExponentialBackOff()).setSleeper(sleeper)); + } +} diff --git a/iot/api-client/mqtt_example/README.md b/iot/api-client/mqtt_example/README.md new file mode 100644 index 00000000000..969813330a9 --- /dev/null +++ b/iot/api-client/mqtt_example/README.md @@ -0,0 +1,52 @@ +# Cloud IoT Core Java MQTT example + +This sample app publishes data to Cloud Pub/Sub using the MQTT bridge provided +as part of Google Cloud IoT Core. + +Note that before you can run the sample, you must configure a Google Cloud +PubSub topic for Cloud IoT Core and register a device as described in the +[parent README](../README.md). + +## Setup + +Run the following command to install the dependencies using Maven: + + mvn clean compile + +## Running the sample + +The following command summarizes the sample usage: + + mvn exec:java \ + -Dexec.mainClass="com.google.cloud.iot.examples.MqttExample" \ + -Dexec.args="-project_id=my-iot-project \ + -registry_id=my-registry \ + -device_id=my-device \ + -private_key_file=rsa_private_pkcs8 \ + -algorithm=RS256" + +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 +your credentials using the [`generate_keys.sh`](../generate_keys.sh) script +provided in the parent folder, you can run the sample as: + + mvn exec:java \ + -Dexec.mainClass="com.google.cloud.iot.examples.MqttExample" \ + -Dexec.args="-project_id=blue-jet-123 \ + -registry_id=my-registry \ + -device_id=my-device \ + -private_key_file=../rsa_private_pkcs8 \ + -algorithm=RS256" + +## Reading the messages written by the sample client + +1. Create a subscription to your topic. + + gcloud beta pubsub subscriptions create \ + projects/your-project-id/subscriptions/my-subscription \ + --topic device-events + +2. Read messages published to the topic + + gcloud beta pubsub subscriptions pull --auto-ack \ + projects/my-iot-project/subscriptions/my-subscription diff --git a/iot/api-client/mqtt_example/pom.xml b/iot/api-client/mqtt_example/pom.xml new file mode 100644 index 00000000000..09b95f4966e --- /dev/null +++ b/iot/api-client/mqtt_example/pom.xml @@ -0,0 +1,54 @@ + + 4.0.0 + com.google.cloud.iot.examples + cloudiot-mqtt-example + jar + 1.0 + cloudiot-mqtt-example + http://maven.apache.org + + + 1.7 + 1.7 + + + + + doc-samples + com.google.cloud + 1.0.0 + ../../../ + + + + + Eclipse Paho Repo + https://repo.eclipse.org/content/repositories/paho-releases/ + + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.0.2 + + + io.jsonwebtoken + jjwt + 0.7.0 + + + joda-time + joda-time + 2.1 + + + commons-cli + commons-cli + 1.3 + + + + diff --git a/iot/api-client/mqtt_example/src/main/java/com/google/cloud/iot/examples/MqttExample.java b/iot/api-client/mqtt_example/src/main/java/com/google/cloud/iot/examples/MqttExample.java new file mode 100644 index 00000000000..09528953587 --- /dev/null +++ b/iot/api-client/mqtt_example/src/main/java/com/google/cloud/iot/examples/MqttExample.java @@ -0,0 +1,133 @@ +package com.google.cloud.iot.examples; + +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.joda.time.DateTime; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; + +/** + * Java sample of connecting to Google Cloud IoT Core vice via MQTT, using JWT. + * + *

This example connects to Google Cloud IoT Core via MQTT, using a JWT for device + * authentication. After connecting, by default the device publishes 100 messages to + * the device's MQTT topic at a rate of one per second, and then exits. + * + *

To run this example, first create your credentials and register your device as + * described in the README located in the sample's parent folder. + * + *

After you have registered your device and generated your credentials, compile and + * run with the corresponding algorithm flag, for example: + * + *

+ *   $ mvn compile
+ *   $ mvn exec:java -Dexec.mainClass="com.google.cloud.iot.examples.MqttExample" \
+ *       -Dexec.args="-project_id=my-project-id \
+ *       -registry_id=my-registry-id \
+ *       -device_id=my-device-id \
+ *       -private_key_file=/path/to/private_pkcs8 \
+ *       -algorithm=RS256"
+ * 
+ */ +public class MqttExample { + /** Load a PKCS8 encoded keyfile from the given path. */ + private static PrivateKey loadKeyFile(String filename, String algorithm) throws Exception { + byte[] keyBytes = Files.readAllBytes(Paths.get(filename)); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(algorithm); + return kf.generatePrivate(spec); + } + + /** Create a Cloud IoT Core JWT for the given project id, signed with the given private key. */ + private static String createJwt(String projectId, String privateKeyFile, String algorithm) + throws Exception { + DateTime now = new DateTime(); + // Create a JWT to authenticate this device. The device will be disconnected after the token + // expires, and will have to reconnect with a new token. The audience field should always be set + // to the GCP project id. + JwtBuilder jwtBuilder = + Jwts.builder() + .setIssuedAt(now.toDate()) + .setExpiration(now.plusMinutes(20).toDate()) + .setAudience(projectId); + + if (algorithm.equals("RS256")) { + PrivateKey privateKey = loadKeyFile(privateKeyFile, "RSA"); + return jwtBuilder.signWith(SignatureAlgorithm.RS256, privateKey).compact(); + } else if (algorithm.equals("ES256")) { + PrivateKey privateKey = loadKeyFile(privateKeyFile, "EC"); + return jwtBuilder.signWith(SignatureAlgorithm.ES256, privateKey).compact(); + } else { + throw new IllegalArgumentException( + "Invalid algorithm " + algorithm + ". Should be one of 'RS256' or 'ES256'."); + } + } + + public static void main(String[] args) throws Exception { + MqttExampleOptions options = MqttExampleOptions.fromFlags(args); + if (options == null) { + // Could not parse. + System.exit(1); + } + + // Build the connection string for Google's Cloud IoT Core MQTT server. Only SSL + // connections are accepted. For server authentication, the JVM's root certificates + // are used. + String mqttServerAddress = + String.format("ssl://%s:%s", options.mqttBridgeHostname, options.mqttBridgePort); + + // Create our MQTT client. The mqttClientId is a unique string that identifies this device. For + // Google Cloud IoT Core, it must be in the format below. + String mqttClientId = + String.format( + "projects/%s/locations/%s/registries/%s/devices/%s", + options.projectId, options.cloudRegion, options.registryId, options.deviceId); + + MqttConnectOptions connectOptions = new MqttConnectOptions(); + // Note that the the Google Cloud IoT Core only supports MQTT 3.1.1, and Paho requires that we + // explictly set this. If you don't set MQTT version, the server will immediately close its + // connection to your device. + connectOptions.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1); + + // With Google Cloud IoT Core, the username field is ignored, however it must be set for the + // Paho client library to send the password field. The password field is used to transmit a JWT + // to authorize the device. + connectOptions.setUserName("unused"); + connectOptions.setPassword( + createJwt(options.projectId, options.privateKeyFile, options.algorithm).toCharArray()); + + // Create a client, and connect to the Google MQTT bridge. + MqttClient client = new MqttClient(mqttServerAddress, mqttClientId, new MemoryPersistence()); + client.connect(connectOptions); + + // The MQTT topic that this device will publish telemetry data to. The MQTT topic name is + // required to be in the format below. Note that this is not the same as the device registry's + // Cloud Pub/Sub topic. + String mqttTopic = String.format("/devices/%s/events", options.deviceId); + + // Publish numMessages messages to the MQTT bridge, at a rate of 1 per second. + for (int i = 1; i <= options.numMessages; ++i) { + String payload = String.format("%s/%s-payload-%d", options.registryId, options.deviceId, i); + System.out.format("Publishing message %d/%d: '%s'\n", i, options.numMessages, payload); + + // Publish "payload" to the MQTT topic. qos=1 means at least once delivery. Cloud IoT Core + // also supports qos=0 for at most once delivery. + MqttMessage message = new MqttMessage(payload.getBytes()); + message.setQos(1); + client.publish(mqttTopic, message); + Thread.sleep(1000); + } + // Disconnect the client and finish the run. + client.disconnect(); + System.out.println("Finished loop successfully. Goodbye!"); + } +} diff --git a/iot/api-client/mqtt_example/src/main/java/com/google/cloud/iot/examples/MqttExampleOptions.java b/iot/api-client/mqtt_example/src/main/java/com/google/cloud/iot/examples/MqttExampleOptions.java new file mode 100644 index 00000000000..472ee15bb47 --- /dev/null +++ b/iot/api-client/mqtt_example/src/main/java/com/google/cloud/iot/examples/MqttExampleOptions.java @@ -0,0 +1,127 @@ +package com.google.cloud.iot.examples; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** Command line options for the MQTT example. */ +public class MqttExampleOptions { + String projectId; + String registryId; + String deviceId; + String privateKeyFile; + String algorithm; + String cloudRegion = "us-central1"; + int numMessages = 100; + String mqttBridgeHostname = "mqtt.googleapis.com"; + short mqttBridgePort = 8883; + + /** Construct an MqttExampleOptions class from command line flags. */ + public static MqttExampleOptions fromFlags(String[] args) { + Options options = new Options(); + // Required arguments + options.addOption( + Option.builder() + .type(String.class) + .longOpt("project_id") + .hasArg() + .desc("GCP cloud project name.") + .required() + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("registry_id") + .hasArg() + .desc("Cloud IoT Core registry id.") + .required() + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("device_id") + .hasArg() + .desc("Cloud IoT Core device id.") + .required() + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("private_key_file") + .hasArg() + .desc("Path to private key file.") + .required() + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("algorithm") + .hasArg() + .desc("Encryption algorithm to use to generate the JWT. Either 'RS256' or 'ES256'.") + .required() + .build()); + + // Optional arguments. + options.addOption( + Option.builder() + .type(String.class) + .longOpt("cloud_region") + .hasArg() + .desc("GCP cloud region.") + .build()); + options.addOption( + Option.builder() + .type(Number.class) + .longOpt("num_messages") + .hasArg() + .desc("Number of messages to publish.") + .build()); + options.addOption( + Option.builder() + .type(String.class) + .longOpt("mqtt_bridge_hostname") + .hasArg() + .desc("MQTT bridge hostname.") + .build()); + options.addOption( + Option.builder() + .type(Number.class) + .longOpt("mqtt_bridge_port") + .hasArg() + .desc("MQTT bridge port.") + .build()); + + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine; + try { + commandLine = parser.parse(options, args); + MqttExampleOptions res = new MqttExampleOptions(); + + res.projectId = commandLine.getOptionValue("project_id"); + res.registryId = commandLine.getOptionValue("registry_id"); + res.deviceId = commandLine.getOptionValue("device_id"); + res.privateKeyFile = commandLine.getOptionValue("private_key_file"); + res.algorithm = commandLine.getOptionValue("algorithm"); + if (commandLine.hasOption("cloud_region")) { + res.cloudRegion = commandLine.getOptionValue("cloud_region"); + } + if (commandLine.hasOption("num_messages")) { + res.numMessages = ((Number) commandLine.getParsedOptionValue("num_messages")).intValue(); + } + if (commandLine.hasOption("mqtt_bridge_hostname")) { + res.mqttBridgeHostname = commandLine.getOptionValue("mqtt_bridge_hostname"); + } + if (commandLine.hasOption("mqtt_bridge_port")) { + res.mqttBridgePort = + ((Number) commandLine.getParsedOptionValue("mqtt_bridge_port")).shortValue(); + } + return res; + } catch (ParseException e) { + System.err.println(e.getMessage()); + return null; + } + } +} diff --git a/iot/api-client/scripts/README.rst b/iot/api-client/scripts/README.rst new file mode 100644 index 00000000000..2d48c4a9aba --- /dev/null +++ b/iot/api-client/scripts/README.rst @@ -0,0 +1,115 @@ +.. This file is automatically generated. Do not edit this file directly. + +Google Cloud IoT Core Python Samples +=============================================================================== + +This directory contains samples for Google Cloud IoT Core. `Google Cloud IoT Core`_ is a fully-managed, globally distributed solution for managing devices and sending / receiving messages from devices. Set the `GOOGLE_CLOUD_PROJECT` environment variable and call the script with your topic ID. + + + + +.. _Google Cloud IoT Core: https://cloud.google.com/iot/ + +Setup +------------------------------------------------------------------------------- + + +Authentication +++++++++++++++ + +Authentication is typically done through `Application Default Credentials`_, +which means you do not have to change the code to authenticate as long as +your environment has credentials. You have a few options for setting up +authentication: + +#. When running locally, use the `Google Cloud SDK`_ + + .. code-block:: bash + + gcloud auth application-default login + + +#. When running on App Engine or Compute Engine, credentials are already + set-up. However, you may need to configure your Compute Engine instance + with `additional scopes`_. + +#. You can create a `Service Account key file`_. This file can be used to + authenticate to Google Cloud Platform services from any environment. To use + the file, set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to + the path to the key file, for example: + + .. code-block:: bash + + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json + +.. _Application Default Credentials: https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow +.. _additional scopes: https://cloud.google.com/compute/docs/authentication#using +.. _Service Account key file: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount + +Install Dependencies +++++++++++++++++++++ + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. + +#. Create a virtualenv. Samples are compatible with Python 2.7 and 3.4+. + + .. code-block:: bash + + $ virtualenv env + $ source env/bin/activate + +#. Install the dependencies needed to run the samples. + + .. code-block:: bash + + $ pip install -r requirements.txt + +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ + +Samples +------------------------------------------------------------------------------- + +PubSub helper ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + + +To run this sample: + +.. code-block:: bash + + $ python iam.py + + usage: iam.py [-h] topic_name + + This application demonstrates how programatically grant access to the + Cloud IoT Core service account on a given PubSub topic. + + For more information, see https://cloud.google.com/iot. + + positional arguments: + topic_name The PubSub topic to grant Cloud IoT Core access to + + optional arguments: + -h, --help show this help message and exit + + + + +The client library +------------------------------------------------------------------------------- + +This sample uses the `Google Cloud Client Library for Python`_. +You can read the documentation for more details on API usage and use GitHub +to `browse the source`_ and `report issues`_. + +.. Google Cloud Client Library for Python: + https://googlecloudplatform.github.io/google-cloud-python/ +.. browse the source: + https://github.com/GoogleCloudPlatform/google-cloud-python +.. report issues: + https://github.com/GoogleCloudPlatform/google-cloud-python/issues + + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ \ No newline at end of file diff --git a/iot/api-client/scripts/README.rst.in b/iot/api-client/scripts/README.rst.in new file mode 100644 index 00000000000..dfd077ff41b --- /dev/null +++ b/iot/api-client/scripts/README.rst.in @@ -0,0 +1,22 @@ +# This file is used to generate README.rst + +product: + name: Google Cloud IoT Core + short_name: Cloud IoT Core + url: https://cloud.google.com/iot/ + description: > + `Google Cloud IoT Core`_ is a fully-managed, globally distributed solution + for managing devices and sending / receiving messages from devices. Set the + `GOOGLE_CLOUD_PROJECT` environment variable and call the script with your + topic ID. + +setup: +- auth +- install_deps + +samples: +- name: PubSub helper + file: iam.py + show_help: true + +cloud_client_library: true diff --git a/iot/api-client/scripts/iam.py b/iot/api-client/scripts/iam.py new file mode 100644 index 00000000000..43cd838b78c --- /dev/null +++ b/iot/api-client/scripts/iam.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +# 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. + +"""This application demonstrates how programatically grant access to the +Cloud IoT Core service account on a given PubSub topic. + +For more information, see https://cloud.google.com/iot. +""" + +import argparse + +from google.cloud import pubsub + + +def set_topic_policy(topic_name): + """Sets the IAM policy for a topic for Cloud IoT Core.""" + pubsub_client = pubsub.Client() + topic = pubsub_client.topic(topic_name) + policy = topic.get_iam_policy() + + # Add a group as publishers. + publishers = policy.get('roles/pubsub.publisher', []) + publishers.append(policy.service_account( + 'cloud-iot@system.gserviceaccount.com')) + policy['roles/pubsub.publisher'] = publishers + + # Set the policy + topic.set_iam_policy(policy) + + print('IAM policy for topic {} set.'.format(topic.name)) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + subparsers = parser.add_argument(dest='topic_name', + help='The PubSub topic to grant Cloud IoT Core access to') + + args = parser.parse_args() + + set_topic_policy(args.topic_name) diff --git a/iot/api-client/scripts/requirements.txt b/iot/api-client/scripts/requirements.txt new file mode 100644 index 00000000000..1412eed3dca --- /dev/null +++ b/iot/api-client/scripts/requirements.txt @@ -0,0 +1 @@ +google-cloud-pubsub==0.25.0