From 8d788c1c6dab53e1681f51910aaf0b89d20b1870 Mon Sep 17 00:00:00 2001 From: Michael Dombrowski Date: Mon, 12 Apr 2021 10:36:15 -0700 Subject: [PATCH] refactor: rewrite IoT cloud helper to use AWS SDK client instead of CRT (#918) --- .../aws/greengrass/iot/IotCloudHelper.java | 135 ++++++------------ .../greengrass/iot/IotConnectionManager.java | 117 ++++----------- .../greengrass/tes/IotCloudHelperTest.java | 92 ++++++------ 3 files changed, 106 insertions(+), 238 deletions(-) diff --git a/src/main/java/com/aws/greengrass/iot/IotCloudHelper.java b/src/main/java/com/aws/greengrass/iot/IotCloudHelper.java index 4a936e63ec..2b298f1652 100644 --- a/src/main/java/com/aws/greengrass/iot/IotCloudHelper.java +++ b/src/main/java/com/aws/greengrass/iot/IotCloudHelper.java @@ -7,45 +7,32 @@ import com.aws.greengrass.deployment.exceptions.AWSIotException; import com.aws.greengrass.iot.model.IotCloudResponse; -import com.aws.greengrass.logging.api.Logger; -import com.aws.greengrass.logging.impl.LogManager; import com.aws.greengrass.util.BaseRetryableAccessor; import com.aws.greengrass.util.CrashableSupplier; import com.aws.greengrass.util.Utils; import lombok.NoArgsConstructor; -import software.amazon.awssdk.crt.http.HttpClientConnection; -import software.amazon.awssdk.crt.http.HttpHeader; -import software.amazon.awssdk.crt.http.HttpRequest; -import software.amazon.awssdk.crt.http.HttpRequestBodyStream; -import software.amazon.awssdk.crt.http.HttpStream; -import software.amazon.awssdk.crt.http.HttpStreamResponseHandler; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.utils.IoUtils; -import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import javax.inject.Singleton; @Singleton @NoArgsConstructor public class IotCloudHelper { - private static final Logger LOGGER = LogManager.getLogger(IotCloudHelper.class); - private static final String HTTP_HEADER_REQUEST_ID = "x-amzn-RequestId"; - private static final String HTTP_HEADER_ERROR_TYPE = "x-amzn-ErrorType"; private static final String HTTP_HEADER_THING_NAME = "x-amzn-iot-thingname"; // TODO: [P41179510]: User configurable network timeout settings // Max wait time for device to receive HTTP response from IOT CLOUD - private static final long TIMEOUT_FOR_RESPONSE_FROM_IOT_CLOUD_SECONDS = Duration.ofSeconds(30).getSeconds(); private static final int RETRY_COUNT = 3; private static final int BACKOFF_MILLIS = 200; @@ -60,87 +47,45 @@ public class IotCloudHelper { * @return Http response corresponding to http request for path * @throws AWSIotException when unable to send the request successfully */ - public IotCloudResponse sendHttpRequest(final IotConnectionManager connManager, String thingName, - final String path, final String verb, final byte[] body) - throws AWSIotException { - List headers = new ArrayList<>(); - headers.add(new HttpHeader("host", connManager.getHost())); - if (Utils.isNotEmpty(thingName)) { - headers.add(new HttpHeader(HTTP_HEADER_THING_NAME, thingName)); - } + public IotCloudResponse sendHttpRequest(final IotConnectionManager connManager, String thingName, final String path, + final String verb, final byte[] body) throws AWSIotException { + SdkHttpRequest.Builder innerRequestBuilder = SdkHttpRequest.builder().method(SdkHttpMethod.fromValue(verb)); - final HttpRequestBodyStream httpRequestBodyStream = body == null ? null : createHttpRequestBodyStream(body); - final HttpRequest request = new HttpRequest(verb, path, headers.toArray(new HttpHeader[0]), - httpRequestBodyStream); - - try (HttpClientConnection conn = connManager.getConnection()) { - BaseRetryableAccessor accessor = new BaseRetryableAccessor(); - CrashableSupplier getHttpResponse = () -> getHttpResponse(conn, request); - return accessor.retry(RETRY_COUNT, BACKOFF_MILLIS, getHttpResponse, - new HashSet<>(Arrays.asList(AWSIotException.class))); + URI uri = connManager.getURI(); + // If the path is actually a full URI, then treat it as such + if (path.startsWith("https://")) { + uri = URI.create(path); + innerRequestBuilder.uri(uri); + } else { + innerRequestBuilder.uri(uri).encodedPath(path); } - } - - private HttpRequestBodyStream createHttpRequestBodyStream(byte[] bytes) { - return new HttpRequestBodyStream() { - @Override - public boolean sendRequestBody(ByteBuffer bodyBytesOut) { - bodyBytesOut.put(bytes); - return true; - } - - @Override - public boolean resetPosition() { - return true; - } - }; - } - private HttpStreamResponseHandler createResponseHandler(CompletableFuture reqCompleted, - Map responseHeaders, - IotCloudResponse response) { - return new HttpStreamResponseHandler() { - @Override - public void onResponseHeaders(HttpStream httpStream, int i, int i1, HttpHeader[] httpHeaders) { - Arrays.stream(httpHeaders).forEach(header -> responseHeaders.put(header.getName(), header.getValue())); - } + if (Utils.isNotEmpty(thingName)) { + innerRequestBuilder.appendHeader(HTTP_HEADER_THING_NAME, thingName); + } - @Override - public int onResponseBody(HttpStream stream, byte[] bodyBytes) { - ByteArrayOutputStream responseByteArray = new ByteArrayOutputStream(); - if (response.getResponseBody() != null) { - responseByteArray.write(response.getResponseBody(), 0, response.getResponseBody().length); - } - responseByteArray.write(bodyBytes, 0, bodyBytes.length); - response.setResponseBody(responseByteArray.toByteArray()); - return bodyBytes.length; - } + ExecutableHttpRequest request = connManager.getClient().prepareRequest(HttpExecuteRequest.builder() + .contentStreamProvider(body == null ? null : () -> new ByteArrayInputStream(body)) + .request(innerRequestBuilder.build()).build()); - @Override - public void onResponseComplete(HttpStream httpStream, int errorCode) { - response.setStatusCode(httpStream.getResponseStatusCode()); - httpStream.close(); - reqCompleted.complete(errorCode); - } - }; + BaseRetryableAccessor accessor = new BaseRetryableAccessor(); + CrashableSupplier getHttpResponse = () -> getHttpResponse(request); + return accessor.retry(RETRY_COUNT, BACKOFF_MILLIS, getHttpResponse, + new HashSet<>(Collections.singletonList(AWSIotException.class))); } - private IotCloudResponse getHttpResponse(HttpClientConnection conn, HttpRequest request) throws AWSIotException { - final CompletableFuture reqCompleted = new CompletableFuture<>(); - final Map responseHeaders = new HashMap<>(); + private IotCloudResponse getHttpResponse(ExecutableHttpRequest request) throws AWSIotException { final IotCloudResponse response = new IotCloudResponse(); - // Give the request up to N seconds to complete, otherwise throw a TimeoutException try { - conn.makeRequest(request, createResponseHandler(reqCompleted, responseHeaders, response)).activate(); - int error = reqCompleted.get(TIMEOUT_FOR_RESPONSE_FROM_IOT_CLOUD_SECONDS, TimeUnit.SECONDS); - if (error != 0) { - throw new AWSIotException(String.format("Error %s(%d); RequestId: %s", HTTP_HEADER_ERROR_TYPE, error, - HTTP_HEADER_REQUEST_ID)); + HttpExecuteResponse httpResponse = request.call(); + response.setStatusCode(httpResponse.httpResponse().statusCode()); + try (AbortableInputStream bodyStream = httpResponse.responseBody() + .orElseThrow(() -> new AWSIotException("No response body"))) { + response.setResponseBody(IoUtils.toByteArray(bodyStream)); } - return response; - } catch (InterruptedException | ExecutionException | TimeoutException e) { - LOGGER.error("Http request failed with error", e); - throw new AWSIotException(e); + } catch (IOException e) { + throw new AWSIotException("Unable to get response", e); } + return response; } } diff --git a/src/main/java/com/aws/greengrass/iot/IotConnectionManager.java b/src/main/java/com/aws/greengrass/iot/IotConnectionManager.java index a0824efbe6..90e0c702a9 100644 --- a/src/main/java/com/aws/greengrass/iot/IotConnectionManager.java +++ b/src/main/java/com/aws/greengrass/iot/IotConnectionManager.java @@ -7,30 +7,14 @@ import com.aws.greengrass.config.WhatHappened; import com.aws.greengrass.deployment.DeviceConfiguration; -import com.aws.greengrass.deployment.exceptions.AWSIotException; -import com.aws.greengrass.logging.api.Logger; -import com.aws.greengrass.logging.impl.LogManager; import com.aws.greengrass.util.Coerce; -import com.aws.greengrass.util.ProxyUtils; -import software.amazon.awssdk.crt.http.HttpClientConnection; -import software.amazon.awssdk.crt.http.HttpClientConnectionManager; -import software.amazon.awssdk.crt.http.HttpClientConnectionManagerOptions; -import software.amazon.awssdk.crt.http.HttpException; -import software.amazon.awssdk.crt.io.ClientBootstrap; -import software.amazon.awssdk.crt.io.EventLoopGroup; -import software.amazon.awssdk.crt.io.HostResolver; -import software.amazon.awssdk.crt.io.SocketOptions; -import software.amazon.awssdk.crt.io.TlsContext; -import software.amazon.awssdk.crt.io.TlsContextOptions; +import software.amazon.awssdk.http.SdkHttpClient; import java.io.Closeable; import java.net.URI; -import java.time.Duration; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import javax.inject.Inject; +import static com.aws.greengrass.componentmanager.ClientConfigurationUtils.getConfiguredClientBuilder; import static com.aws.greengrass.deployment.DeviceConfiguration.DEVICE_MQTT_NAMESPACE; import static com.aws.greengrass.deployment.DeviceConfiguration.DEVICE_PARAM_AWS_REGION; import static com.aws.greengrass.deployment.DeviceConfiguration.DEVICE_PARAM_CERTIFICATE_FILE_PATH; @@ -38,17 +22,10 @@ import static com.aws.greengrass.deployment.DeviceConfiguration.DEVICE_PARAM_PRIVATE_KEY_PATH; import static com.aws.greengrass.deployment.DeviceConfiguration.DEVICE_PARAM_ROOT_CA_PATH; import static com.aws.greengrass.deployment.DeviceConfiguration.DEVICE_PARAM_THING_NAME; -import static com.aws.greengrass.mqttclient.MqttClient.EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS; public class IotConnectionManager implements Closeable { - private static final Logger LOGGER = LogManager.getLogger(IotConnectionManager.class); - // Max wait time for device to establish mTLS connection with IOT core - private static final long TIMEOUT_FOR_CONNECTION_SETUP_SECONDS = Duration.ofMinutes(1).getSeconds(); - private HttpClientConnectionManager connManager; - - private final EventLoopGroup eventLoopGroup; - private final HostResolver resolver; - private final ClientBootstrap clientBootstrap; + private final DeviceConfiguration deviceConfiguration; + private SdkHttpClient client; /** * Constructor. @@ -58,89 +35,45 @@ public class IotConnectionManager implements Closeable { @Inject @SuppressWarnings("PMD.AvoidCatchingGenericException") public IotConnectionManager(final DeviceConfiguration deviceConfiguration) { - eventLoopGroup = new EventLoopGroup(1); - resolver = new HostResolver(eventLoopGroup); - clientBootstrap = new ClientBootstrap(eventLoopGroup, resolver); - try { - this.connManager = initConnectionManager(deviceConfiguration); - reconfigureOnConfigChange(deviceConfiguration); - } catch (RuntimeException e) { - // If we couldn't initialize the connection manager, then make sure to shutdown - // everything which was started up - clientBootstrap.close(); - resolver.close(); - eventLoopGroup.close(); - throw e; - } + this.deviceConfiguration = deviceConfiguration; + this.client = initConnectionManager(); + reconfigureOnConfigChange(); } - private void reconfigureOnConfigChange(DeviceConfiguration deviceConfiguration) { + public URI getURI() { + return URI.create("https://" + Coerce.toString(deviceConfiguration.getIotCredentialEndpoint())); + } + + public synchronized SdkHttpClient getClient() { + return this.client; + } + + private void reconfigureOnConfigChange() { deviceConfiguration.onAnyChange((what, node) -> { if (WhatHappened.childChanged.equals(what) && node != null && (node.childOf(DEVICE_MQTT_NAMESPACE) || node .childOf(DEVICE_PARAM_THING_NAME) || node.childOf(DEVICE_PARAM_IOT_DATA_ENDPOINT) || node .childOf(DEVICE_PARAM_PRIVATE_KEY_PATH) || node.childOf(DEVICE_PARAM_CERTIFICATE_FILE_PATH) || node .childOf(DEVICE_PARAM_ROOT_CA_PATH) || node.childOf(DEVICE_PARAM_AWS_REGION))) { - this.connManager = initConnectionManager(deviceConfiguration); + synchronized (this) { + this.client.close(); + this.client = initConnectionManager(); + } } }); } - private HttpClientConnectionManager initConnectionManager(DeviceConfiguration deviceConfiguration) { - final String certPath = Coerce.toString(deviceConfiguration.getCertificateFilePath()); - final String keyPath = Coerce.toString(deviceConfiguration.getPrivateKeyFilePath()); - final String caPath = Coerce.toString(deviceConfiguration.getRootCAFilePath()); - try (TlsContextOptions tlsCtxOptions = TlsContextOptions.createWithMtlsFromPath(certPath, keyPath)) { - tlsCtxOptions.overrideDefaultTrustStoreFromPath(null, caPath); - return HttpClientConnectionManager - .create(new HttpClientConnectionManagerOptions().withClientBootstrap(clientBootstrap) - .withProxyOptions(ProxyUtils.getHttpProxyOptions(deviceConfiguration)) - .withSocketOptions(new SocketOptions()).withTlsContext(new TlsContext(tlsCtxOptions)) - .withUri(URI.create( - "https://" + Coerce.toString(deviceConfiguration.getIotCredentialEndpoint())))); - } + private SdkHttpClient initConnectionManager() { + return getConfiguredClientBuilder(deviceConfiguration).build(); } - /** - * Get a connection object for sending requests. - * - * @return {@link HttpClientConnection} - * @throws AWSIotException when getting a connection from underlying manager fails. - */ - public HttpClientConnection getConnection() throws AWSIotException { - try { - return connManager.acquireConnection().get(TIMEOUT_FOR_CONNECTION_SETUP_SECONDS, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException | HttpException e) { - LOGGER.error("Getting connection failed for endpoint {} with error {} ", connManager.getUri(), e); - throw new AWSIotException(e); - } - } - - /** - * Get the host string underlying connection manager. - * - * @return Host string to be used in HTTP Host headers - */ - public String getHost() { - return connManager.getUri().getHost(); - } /** * Clean up underlying connections and close gracefully. */ @Override - public void close() { - connManager.close(); - clientBootstrap.close(); - resolver.close(); - eventLoopGroup.close(); - try { - eventLoopGroup.getShutdownCompleteFuture().get(EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - LOGGER.atError().log("Error shutting down event loop", e); - } catch (TimeoutException e) { - LOGGER.atError().log("Timed out shutting down event loop"); + public synchronized void close() { + if (this.client != null) { + this.client.close(); } } } diff --git a/src/test/java/com/aws/greengrass/tes/IotCloudHelperTest.java b/src/test/java/com/aws/greengrass/tes/IotCloudHelperTest.java index 20239b3a03..7b4600310c 100644 --- a/src/test/java/com/aws/greengrass/tes/IotCloudHelperTest.java +++ b/src/test/java/com/aws/greengrass/tes/IotCloudHelperTest.java @@ -13,50 +13,51 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import software.amazon.awssdk.crt.http.HttpClientConnection; -import software.amazon.awssdk.crt.http.HttpRequest; -import software.amazon.awssdk.crt.http.HttpStream; -import software.amazon.awssdk.crt.http.HttpStreamResponseHandler; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpResponse; -import java.nio.ByteBuffer; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith({MockitoExtension.class, GGExtension.class}) class IotCloudHelperTest { private static final byte[] CLOUD_RESPONSE = "HELLO WORLD".getBytes(StandardCharsets.UTF_8); private static final int STATUS_CODE = 200; - private static final String HOST = "localhost"; + private static final URI HOST = URI.create("http://localhost"); private static final String IOT_CREDENTIALS_PATH = "MOCK_PATH/get.json"; @Mock IotConnectionManager mockConnectionManager; @Mock - HttpClientConnection mockConnection; - - @Mock - HttpStream mockHttpStream; + SdkHttpClient mockClient; @Test void GIVEN_valid_creds_WHEN_send_request_called_THEN_success() throws Exception { - when(mockConnectionManager.getConnection()).thenReturn(mockConnection); - when(mockConnectionManager.getHost()).thenReturn(HOST); - when(mockHttpStream.getResponseStatusCode()).thenReturn(STATUS_CODE); - doAnswer(invocationArgs -> { - HttpStreamResponseHandler handler = (HttpStreamResponseHandler)invocationArgs.getArguments()[1]; - handler.onResponseBody(mockHttpStream, CLOUD_RESPONSE); - handler.onResponseComplete(mockHttpStream, 0); - return mockHttpStream; - }).when(mockConnection).makeRequest(any(), any()); + when(mockConnectionManager.getClient()).thenReturn(mockClient); + when(mockConnectionManager.getURI()).thenReturn(HOST); + + ExecutableHttpRequest requestMock = mock(ExecutableHttpRequest.class); + when(requestMock.call()).thenReturn( + HttpExecuteResponse.builder().response(SdkHttpResponse.builder().statusCode(STATUS_CODE).build()) + .responseBody(AbortableInputStream.create(new ByteArrayInputStream(CLOUD_RESPONSE))).build()); + + doReturn(requestMock).when(mockClient).prepareRequest(any()); IotCloudHelper cloudHelper = new IotCloudHelper(); - final IotCloudResponse response = cloudHelper.sendHttpRequest(mockConnectionManager, null, - IOT_CREDENTIALS_PATH, CredentialRequestHandler.IOT_CREDENTIALS_HTTP_VERB, null); + final IotCloudResponse response = cloudHelper.sendHttpRequest(mockConnectionManager, null, IOT_CREDENTIALS_PATH, + CredentialRequestHandler.IOT_CREDENTIALS_HTTP_VERB, null); assertArrayEquals(CLOUD_RESPONSE, response.getResponseBody()); assertEquals(STATUS_CODE, response.getStatusCode()); } @@ -64,41 +65,30 @@ void GIVEN_valid_creds_WHEN_send_request_called_THEN_success() throws Exception @Test void GIVEN_valid_creds_WHEN_send_request_called_with_body_THEN_success() throws Exception { byte[] body = "hello".getBytes(StandardCharsets.UTF_8); - when(mockConnectionManager.getConnection()).thenReturn(mockConnection); - when(mockConnectionManager.getHost()).thenReturn(HOST); - doAnswer(invocationArgs -> { - HttpRequest request = invocationArgs.getArgument(0); - ByteBuffer byteBuffer = ByteBuffer.allocate(body.length); - request.getBodyStream().sendRequestBody(byteBuffer); - assertArrayEquals(body, byteBuffer.array()); - HttpStreamResponseHandler handler = invocationArgs.getArgument(1); - handler.onResponseBody(mockHttpStream, CLOUD_RESPONSE); - handler.onResponseComplete(mockHttpStream, 0); - return mockHttpStream; - }).when(mockConnection).makeRequest(any(), any()); + when(mockConnectionManager.getClient()).thenReturn(mockClient); + when(mockConnectionManager.getURI()).thenReturn(HOST); + ExecutableHttpRequest requestMock = mock(ExecutableHttpRequest.class); + when(requestMock.call()).thenReturn( + HttpExecuteResponse.builder().response(SdkHttpResponse.builder().statusCode(STATUS_CODE).build()) + .responseBody(AbortableInputStream.create(new ByteArrayInputStream(CLOUD_RESPONSE))).build()); + + doReturn(requestMock).when(mockClient).prepareRequest(any()); IotCloudHelper cloudHelper = new IotCloudHelper(); - final byte[] creds = cloudHelper.sendHttpRequest(mockConnectionManager, null, - IOT_CREDENTIALS_PATH, CredentialRequestHandler.IOT_CREDENTIALS_HTTP_VERB, body).getResponseBody(); + final byte[] creds = cloudHelper.sendHttpRequest(mockConnectionManager, null, IOT_CREDENTIALS_PATH, + CredentialRequestHandler.IOT_CREDENTIALS_HTTP_VERB, body).getResponseBody(); assertArrayEquals(CLOUD_RESPONSE, creds); } @Test void GIVEN_error_code_once_WHEN_send_request_called_THEN_retry_and_success() throws Exception { - when(mockConnectionManager.getConnection()).thenReturn(mockConnection); - when(mockConnectionManager.getHost()).thenReturn(HOST); - when(mockHttpStream.getResponseStatusCode()).thenReturn(STATUS_CODE); - doAnswer(invocationArgs -> { - HttpStreamResponseHandler handler = (HttpStreamResponseHandler) invocationArgs.getArguments()[1]; - handler.onResponseBody(mockHttpStream, CLOUD_RESPONSE); - // error code not 0, fail to complete - handler.onResponseComplete(mockHttpStream, 1); - return mockHttpStream; - }).doAnswer(invocationArgs -> { - HttpStreamResponseHandler handler = (HttpStreamResponseHandler) invocationArgs.getArguments()[1]; - handler.onResponseBody(mockHttpStream, CLOUD_RESPONSE); - handler.onResponseComplete(mockHttpStream, 0); - return mockHttpStream; - }).when(mockConnection).makeRequest(any(), any()); + when(mockConnectionManager.getClient()).thenReturn(mockClient); + when(mockConnectionManager.getURI()).thenReturn(HOST); + ExecutableHttpRequest requestMock = mock(ExecutableHttpRequest.class); + when(requestMock.call()).thenThrow(IOException.class).thenReturn( + HttpExecuteResponse.builder().response(SdkHttpResponse.builder().statusCode(STATUS_CODE).build()) + .responseBody(AbortableInputStream.create(new ByteArrayInputStream(CLOUD_RESPONSE))).build()); + + doReturn(requestMock).when(mockClient).prepareRequest(any()); IotCloudHelper cloudHelper = new IotCloudHelper(); final IotCloudResponse response = cloudHelper.sendHttpRequest(mockConnectionManager, null, IOT_CREDENTIALS_PATH, CredentialRequestHandler.IOT_CREDENTIALS_HTTP_VERB, null);