use of org.eclipse.hono.service.http.HttpContext in project hono by eclipse.
the class AbstractVertxBasedHttpProtocolAdapterTest method testUploadEmptyCommandResponseSucceeds.
/**
* Verifies that the adapter accepts a command response message with an empty body.
*/
@Test
public void testUploadEmptyCommandResponseSucceeds() {
// GIVEN an adapter with a downstream application attached
givenACommandResponseSenderForAnyTenant();
givenAnAdapter(properties);
// WHEN a device publishes a command response with an empty payload
final Buffer payload = null;
final HttpServerResponse response = mock(HttpServerResponse.class);
final HttpContext ctx = newHttpContext(payload, "application/text", mock(HttpServerRequest.class), response);
when(ctx.getRoutingContext().addBodyEndHandler(VertxMockSupport.anyHandler())).thenAnswer(invocation -> {
final Handler<Void> handler = invocation.getArgument(0);
handler.handle(null);
return 0;
});
adapter.uploadCommandResponseMessage(ctx, "tenant", "device", CMD_REQ_ID, 200);
// then it is forwarded successfully
verify(response).setStatusCode(202);
verify(response).end();
verify(metrics).reportCommand(eq(Direction.RESPONSE), eq("tenant"), any(), eq(ProcessingOutcome.FORWARDED), eq(0), any());
}
use of org.eclipse.hono.service.http.HttpContext in project hono by eclipse.
the class AbstractVertxBasedHttpProtocolAdapterTest method testUploadCommandResponseFailsForRejectedOutcome.
/**
* Verifies that the adapter fails the upload of a command response with a 400
* result if it is rejected by the downstream peer.
*/
@Test
public void testUploadCommandResponseFailsForRejectedOutcome() {
// GIVEN an adapter with a downstream application attached
final Promise<Void> outcome = Promise.promise();
givenACommandResponseSenderForAnyTenant(outcome);
givenAnAdapter(properties);
// WHEN a device publishes a command response that is not accepted by the application
final Buffer payload = Buffer.buffer("some payload");
final HttpContext ctx = newHttpContext(payload, "application/text", mock(HttpServerRequest.class), mock(HttpServerResponse.class));
adapter.uploadCommandResponseMessage(ctx, "tenant", "device", CMD_REQ_ID, 200);
outcome.fail(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "malformed message"));
// THEN the device gets a 400
assertContextFailedWithClientError(ctx, HttpURLConnection.HTTP_BAD_REQUEST);
// and the response has not been reported as forwarded
verify(metrics).reportCommand(eq(Direction.RESPONSE), eq("tenant"), any(), eq(ProcessingOutcome.UNPROCESSABLE), eq(payload.length()), any());
}
use of org.eclipse.hono.service.http.HttpContext in project hono by eclipse.
the class HonoBasicAuthHandlerTest method testPreCredentialsValidationHandlerGetsInvoked.
/**
* Verifies that the PreCredentialsValidationHandler given for the AuthHandler is invoked
* when authenticating.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testPreCredentialsValidationHandlerGetsInvoked() {
final AbstractDeviceCredentials deviceCredentials = mock(AbstractDeviceCredentials.class);
final DeviceUser deviceUser = new DeviceUser("tenant", "device");
// prepare authProvider
final DeviceCredentialsAuthProvider<AbstractDeviceCredentials> authProvider = mock(DeviceCredentialsAuthProvider.class);
doReturn(deviceCredentials).when(authProvider).getCredentials(any(JsonObject.class));
doAnswer(invocation -> {
final Handler handler = invocation.getArgument(2);
handler.handle(Future.succeededFuture(deviceUser));
return null;
}).when(authProvider).authenticate(any(AbstractDeviceCredentials.class), any(), VertxMockSupport.anyHandler());
// prepare PreCredentialsValidationHandler
final PreCredentialsValidationHandler<HttpContext> preCredValidationHandler = mock(PreCredentialsValidationHandler.class);
when(preCredValidationHandler.handle(eq(deviceCredentials), any(HttpContext.class))).thenReturn(Future.succeededFuture());
// GIVEN an auth handler with a PreCredentialsValidationHandler
final HonoBasicAuthHandler authHandler = new HonoBasicAuthHandler(authProvider, "test", preCredValidationHandler);
// WHEN the auth handler handles a request
final String authorization = "BASIC " + Base64.getEncoder().encodeToString("user:password".getBytes(StandardCharsets.UTF_8));
final MultiMap headers = mock(MultiMap.class);
when(headers.get(eq(HttpHeaders.AUTHORIZATION))).thenReturn(authorization);
final HttpServerRequest req = mock(HttpServerRequest.class);
when(req.headers()).thenReturn(headers);
final HttpServerResponse resp = mock(HttpServerResponse.class);
final RoutingContext routingContext = mock(RoutingContext.class);
final Map<String, Object> routingContextMap = new HashMap<>();
when(routingContext.put(any(), any())).thenAnswer(invocation -> {
routingContextMap.put(invocation.getArgument(0), invocation.getArgument(1));
return routingContext;
});
when(routingContext.get(any())).thenAnswer(invocation -> routingContextMap.get(invocation.getArgument(0)));
when(routingContext.request()).thenReturn(req);
when(routingContext.response()).thenReturn(resp);
when(routingContext.currentRoute()).thenReturn(mock(Route.class));
authHandler.handle(routingContext);
// THEN authentication succeeds and the PreCredentialsValidationHandler has been invoked
verify(routingContext).setUser(eq(deviceUser));
verify(preCredValidationHandler).handle(eq(deviceCredentials), any(HttpContext.class));
}
use of org.eclipse.hono.service.http.HttpContext in project hono by eclipse.
the class AbstractVertxBasedHttpProtocolAdapter method doUploadMessage.
private void doUploadMessage(final HttpContext ctx, final String tenant, final String deviceId, final Buffer payload, final String contentType, final MetricsTags.EndpointType endpoint) {
if (!ctx.hasValidQoS()) {
HttpUtils.badRequest(ctx.getRoutingContext(), "unsupported QoS-Level header value");
return;
}
if (!isPayloadOfIndicatedType(payload, contentType)) {
HttpUtils.badRequest(ctx.getRoutingContext(), String.format("content type [%s] does not match payload", contentType));
return;
}
final MetricsTags.QoS qos = getQoSLevel(endpoint, ctx.getRequestedQos());
final Device authenticatedDevice = ctx.getAuthenticatedDevice();
final String gatewayId = authenticatedDevice != null && !deviceId.equals(authenticatedDevice.getDeviceId()) ? authenticatedDevice.getDeviceId() : null;
final Span currentSpan = TracingHelper.buildChildSpan(tracer, TracingHandler.serverSpanContext(ctx.getRoutingContext()), "upload " + endpoint.getCanonicalName(), getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenant).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).withTag(TracingHelper.TAG_AUTHENTICATED.getKey(), authenticatedDevice != null).withTag(TracingHelper.TAG_QOS, qos.name()).start();
final Promise<Void> responseReady = Promise.promise();
final Future<RegistrationAssertion> tokenTracker = getRegistrationAssertion(tenant, deviceId, authenticatedDevice, currentSpan.context());
final int payloadSize = Optional.ofNullable(payload).map(ok -> payload.length()).orElse(0);
final Future<TenantObject> tenantTracker = getTenantConfiguration(tenant, currentSpan.context());
final Future<TenantObject> tenantValidationTracker = tenantTracker.compose(tenantObject -> CompositeFuture.all(isAdapterEnabled(tenantObject), checkMessageLimit(tenantObject, payloadSize, currentSpan.context())).map(success -> tenantObject));
// we only need to consider TTD if the device and tenant are enabled and the adapter
// is enabled for the tenant
final Future<Integer> ttdTracker = CompositeFuture.all(tenantValidationTracker, tokenTracker).compose(ok -> {
final Integer ttdParam = getTimeUntilDisconnectFromRequest(ctx);
return getTimeUntilDisconnect(tenantTracker.result(), ttdParam).map(effectiveTtd -> {
if (effectiveTtd != null) {
currentSpan.setTag(MessageHelper.APP_PROPERTY_DEVICE_TTD, effectiveTtd);
}
return effectiveTtd;
});
});
final Future<CommandConsumer> commandConsumerTracker = ttdTracker.compose(ttd -> createCommandConsumer(ttd, tenantTracker.result(), deviceId, gatewayId, ctx.getRoutingContext(), responseReady, currentSpan));
commandConsumerTracker.compose(ok -> {
final Map<String, Object> props = getDownstreamMessageProperties(ctx);
Optional.ofNullable(commandConsumerTracker.result()).map(c -> ttdTracker.result()).ifPresent(ttd -> props.put(MessageHelper.APP_PROPERTY_DEVICE_TTD, ttd));
props.put(MessageHelper.APP_PROPERTY_QOS, ctx.getRequestedQos().ordinal());
customizeDownstreamMessageProperties(props, ctx);
setTtdRequestConnectionCloseHandler(ctx.getRoutingContext(), commandConsumerTracker.result(), tenant, deviceId, currentSpan);
if (EndpointType.EVENT.equals(endpoint)) {
ctx.getTimeToLive().ifPresent(ttl -> props.put(MessageHelper.SYS_HEADER_PROPERTY_TTL, ttl.toSeconds()));
return CompositeFuture.all(getEventSender(tenantValidationTracker.result()).sendEvent(tenantTracker.result(), tokenTracker.result(), contentType, payload, props, currentSpan.context()), responseReady.future()).map(s -> (Void) null);
} else {
// unsettled
return CompositeFuture.all(getTelemetrySender(tenantValidationTracker.result()).sendTelemetry(tenantTracker.result(), tokenTracker.result(), ctx.getRequestedQos(), contentType, payload, props, currentSpan.context()), responseReady.future()).map(s -> (Void) null);
}
}).compose(proceed -> {
// request and the CommandConsumer from the current request has not been closed yet
return Optional.ofNullable(commandConsumerTracker.result()).map(consumer -> consumer.close(currentSpan.context()).otherwise(thr -> {
TracingHelper.logError(currentSpan, thr);
return (Void) null;
})).orElseGet(Future::succeededFuture);
}).map(proceed -> {
if (ctx.response().closed()) {
log.debug("failed to send http response for [{}] message from device [tenantId: {}, deviceId: {}]: response already closed", endpoint, tenant, deviceId);
TracingHelper.logError(currentSpan, "failed to send HTTP response to device: response already closed");
currentSpan.finish();
// close the response here, ensuring that the TracingHandler bodyEndHandler gets called
ctx.response().end();
} else {
final CommandContext commandContext = ctx.get(CommandContext.KEY_COMMAND_CONTEXT);
setResponsePayload(ctx.response(), commandContext, currentSpan);
ctx.getRoutingContext().addBodyEndHandler(ok -> {
log.trace("successfully processed [{}] message for device [tenantId: {}, deviceId: {}]", endpoint, tenant, deviceId);
if (commandContext != null) {
commandContext.getTracingSpan().log("forwarded command to device in HTTP response body");
commandContext.accept();
metrics.reportCommand(commandContext.getCommand().isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenant, tenantTracker.result(), ProcessingOutcome.FORWARDED, commandContext.getCommand().getPayloadSize(), getMicrometerSample(commandContext));
}
metrics.reportTelemetry(endpoint, tenant, tenantTracker.result(), ProcessingOutcome.FORWARDED, qos, payloadSize, ctx.getTtdStatus(), getMicrometerSample(ctx.getRoutingContext()));
currentSpan.finish();
});
ctx.response().exceptionHandler(t -> {
log.debug("failed to send http response for [{}] message from device [tenantId: {}, deviceId: {}]", endpoint, tenant, deviceId, t);
if (commandContext != null) {
TracingHelper.logError(commandContext.getTracingSpan(), "failed to forward command to device in HTTP response body", t);
commandContext.release(t);
metrics.reportCommand(commandContext.getCommand().isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenant, tenantTracker.result(), ProcessingOutcome.UNDELIVERABLE, commandContext.getCommand().getPayloadSize(), getMicrometerSample(commandContext));
}
currentSpan.log("failed to send HTTP response to device");
TracingHelper.logError(currentSpan, t);
currentSpan.finish();
});
ctx.response().end();
}
return proceed;
}).recover(t -> {
log.debug("cannot process [{}] message from device [tenantId: {}, deviceId: {}]", endpoint, tenant, deviceId, t);
final boolean responseClosedPrematurely = ctx.response().closed();
final Future<Void> commandConsumerClosedTracker = Optional.ofNullable(commandConsumerTracker.result()).map(consumer -> consumer.close(currentSpan.context()).onFailure(thr -> TracingHelper.logError(currentSpan, thr))).orElseGet(Future::succeededFuture);
final CommandContext commandContext = ctx.get(CommandContext.KEY_COMMAND_CONTEXT);
if (commandContext != null) {
TracingHelper.logError(commandContext.getTracingSpan(), "command won't be forwarded to device in HTTP response body, HTTP request handling failed", t);
commandContext.release(t);
currentSpan.log("released command for device");
}
final ProcessingOutcome outcome;
if (ClientErrorException.class.isInstance(t)) {
outcome = ProcessingOutcome.UNPROCESSABLE;
ctx.fail(t);
} else {
outcome = ProcessingOutcome.UNDELIVERABLE;
final String errorMessage = t instanceof ServerErrorException ? ((ServerErrorException) t).getClientFacingMessage() : null;
HttpUtils.serviceUnavailable(ctx.getRoutingContext(), 2, Strings.isNullOrEmpty(errorMessage) ? "temporarily unavailable" : errorMessage);
}
if (responseClosedPrematurely) {
log.debug("failed to send http response for [{}] message from device [tenantId: {}, deviceId: {}]: response already closed", endpoint, tenant, deviceId);
TracingHelper.logError(currentSpan, "failed to send HTTP response to device: response already closed");
}
metrics.reportTelemetry(endpoint, tenant, tenantTracker.result(), outcome, qos, payloadSize, ctx.getTtdStatus(), getMicrometerSample(ctx.getRoutingContext()));
TracingHelper.logError(currentSpan, t);
commandConsumerClosedTracker.onComplete(res -> currentSpan.finish());
return Future.failedFuture(t);
});
}
use of org.eclipse.hono.service.http.HttpContext in project hono by eclipse.
the class AbstractVertxBasedHttpProtocolAdapter method handleBeforeCredentialsValidation.
/**
* Handles any operations that should be invoked as part of the authentication process after the credentials got
* determined and before they get validated. Can be used to perform checks using the credentials and tenant
* information before the potentially expensive credentials validation is done
* <p>
* The default implementation updates the trace sampling priority in the execution context tracing span.
* It also verifies that the tenant provided via the credentials is enabled and that the adapter is enabled for
* that tenant, failing the returned future if either is not the case.
* <p>
* Subclasses should override this method in order to perform additional operations after calling this super method.
*
* @param credentials The credentials.
* @param executionContext The execution context, including the TenantObject.
* @return A future indicating the outcome of the operation. A failed future will fail the authentication attempt.
*/
protected Future<Void> handleBeforeCredentialsValidation(final DeviceCredentials credentials, final HttpContext executionContext) {
final String tenantId = credentials.getTenantId();
final String authId = credentials.getAuthId();
final Span span = Optional.ofNullable(executionContext.getTracingSpan()).orElseGet(() -> {
log.warn("handleBeforeCredentialsValidation: no span context set in httpContext");
return NoopSpan.INSTANCE;
});
return getTenantConfiguration(tenantId, span.context()).recover(t -> Future.failedFuture(CredentialsApiAuthProvider.mapNotFoundToBadCredentialsException(t))).map(tenantObject -> {
TracingHelper.setDeviceTags(span, tenantId, null, authId);
TenantTraceSamplingHelper.applyTraceSamplingPriority(tenantObject, authId, span);
return tenantObject;
}).compose(tenantObject -> isAdapterEnabled(tenantObject)).mapEmpty();
}
Aggregations