SDK: Add unified Link download (using auth contexts) (#2270)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-02-12 18:32:18 +02:00
committed by GitHub
parent 2f418cc65b
commit 97027de9a8
9 changed files with 295 additions and 329 deletions

View File

@@ -14,6 +14,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.ref.Cleaner;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@@ -28,37 +30,50 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Client;
import feign.Contract;
import feign.Feign;
import feign.FeignException;
import feign.Request;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.hc5.ApacheHttp5Client;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -95,7 +110,6 @@ public class HawkbitClient {
};
private final HawkbitServer hawkBitServer;
private final Client client;
private final Encoder encoder;
private final Decoder decoder;
private final Contract contract;
@@ -104,21 +118,18 @@ public class HawkbitClient {
private final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn;
public HawkbitClient(
final HawkbitServer hawkBitServer,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
this(hawkBitServer, client, encoder, decoder, contract, null, null);
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
this(hawkBitServer, encoder, decoder, contract, null, null);
}
/**
* Customizers gets default ones and could
*/
public HawkbitClient(
final HawkbitServer hawkBitServer,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract,
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract,
final ErrorDecoder errorDecoder,
final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn) {
this.hawkBitServer = hawkBitServer;
this.client = client;
this.encoder = encoder;
this.decoder = decoder;
this.contract = contract;
@@ -135,6 +146,43 @@ public class HawkbitClient {
return service(serviceType, tenantProperties, controller);
}
public static <T> T getLink(final Link link, final Class<T> linkType, final Tenant tenant, final Controller controller) throws IOException {
final String url = link.getHref();
final HttpClientKey key = new HttpClientKey(
url.startsWith("https://"), controller == null ? null : controller.getCertificate(), tenant.getTenantCA());
final HttpClient httpClient = httpClient(key);
try {
final HttpGet request = new HttpGet(url);
final String gatewayToken = tenant.getGatewayToken();
if (StringUtils.hasLength(gatewayToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "GatewayToken " + gatewayToken);
} else {
final String targetToken = controller == null ? null : controller.getSecurityToken();
if (StringUtils.hasLength(targetToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "TargetToken " + targetToken);
}
} // else not authenticated or certificate based
return httpClient.execute(request, response -> {
if (response.getCode() != HttpStatus.OK.value()) {
throw new IllegalStateException("Unexpected status code: " + response.getCode());
}
if (linkType.isAssignableFrom(response.getClass())) {
return (T)response;
} else if (linkType == InputStream.class) {
return (T)response.getEntity().getContent();
} else {
return new ObjectMapper().readValue(response.getEntity().getContent(), linkType);
}
});
} finally {
synchronized (HTTP_CLIENTS) {
HTTP_CLIENTS.get(key).release();
}
}
}
private <T> T service(final Class<T> serviceType, final Tenant tenant, final Controller controller) {
final T service = service0(serviceType, tenant, controller);
if (serviceType.isInterface() // proxy only interfaces
@@ -149,27 +197,26 @@ public class HawkbitClient {
}
}
private static final Cleaner CLEANER = Cleaner.create();
private <T> T service0(final Class<T> serviceType, final Tenant tenant, final Controller controller) {
final Client client;
if (controller != null && controller.getCertificate() != null && hawkBitServer.getDdiUrl().startsWith("https://")) {
// mTLS could be requested
try {
client = mTlsClient(controller.getCertificate(), tenant);
} catch (final RuntimeException | Error e) {
throw e;
} catch (final Exception e) {
throw new IllegalStateException("Failed to create mTLS client", e);
}
} else {
client = this.client;
}
return Feign.builder().client(client)
final String url = controller == null ? hawkBitServer.getMgmtUrl() : hawkBitServer.getDdiUrl();
final HttpClientKey key = new HttpClientKey(
url.startsWith("https://"), controller == null ? null : controller.getCertificate(), tenant.getTenantCA());
final HttpClient httpClient = httpClient(key);
final T service = Feign.builder()
.client(new ApacheHttp5Client(httpClient))
.encoder(encoder)
.decoder(decoder)
.errorDecoder(errorDecoder)
.contract(contract)
.requestInterceptor(requestInterceptorFn.apply(tenant, controller))
.target(serviceType, controller == null ? hawkBitServer.getMgmtUrl() : hawkBitServer.getDdiUrl());
.target(serviceType, url);
CLEANER.register(service, () -> {
synchronized (HTTP_CLIENTS) {
HTTP_CLIENTS.get(key).release();
}
});
return service;
}
@SuppressWarnings("unchecked")
@@ -339,25 +386,95 @@ public class HawkbitClient {
random.nextBytes(bytes);
KEYSTORE_PASSWORD = Base64.getEncoder().encodeToString(bytes);
}
private static Client mTlsClient(final Certificate certificate, final Tenant tenant)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, CertificateException, IOException,
KeyManagementException {
final KeyStore clientKeyStore = certificate.toKeyStore(KEYSTORE_PASSWORD);
final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, KEYSTORE_PASSWORD.toCharArray());
// Truststore
final TrustManagerFactory trustManagerFactory;
if (tenant.getDdiCertificate() == null) {
trustManagerFactory = null;
private static final Map<HttpClientKey, HttpClientWrapper> HTTP_CLIENTS = new HashMap<>();
private static HttpClient httpClient(final HttpClientKey key) {
synchronized (HTTP_CLIENTS) {
final HttpClientWrapper httpClientWrapper = HTTP_CLIENTS.get(key);
HttpClient client = httpClientWrapper == null ? null : httpClientWrapper.get();
if (client == null) { // create
final CloseableHttpClient newClient;
if (key.isHttps()) {
// mTLS could be requested
try {
newClient = tlsClient(key.getClientCertificate(), key.getServerCertificates());
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new IllegalStateException("Failed to create mTLS client", e);
}
} else {
newClient = HttpClients.createDefault();
}
HTTP_CLIENTS.put(key, new HttpClientWrapper(key, newClient));
return newClient;
} else {
return client; // reuse
}
}
}
private static CloseableHttpClient tlsClient(final Certificate clientCertificate, final X509Certificate[] serverCertificates)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException, CertificateException,
IOException {
final SSLContextBuilder sslContextBuilder = SSLContextBuilder.create();
if (clientCertificate != null) {
sslContextBuilder.loadKeyMaterial(clientCertificate.toKeyStore(KEYSTORE_PASSWORD), KEYSTORE_PASSWORD.toCharArray());
}
if (serverCertificates == null) {
// trust all
sslContextBuilder.loadTrustMaterial(null, new TrustAllStrategy());
} else {
final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
trustStore.setEntry("alias", new KeyStore.TrustedCertificateEntry(tenant.getDdiCertificate()), null);
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
for (int i = 0; i < serverCertificates.length; i++) {
trustStore.setEntry("alias" + i, new KeyStore.TrustedCertificateEntry(serverCertificates[i]), null);
}
sslContextBuilder.loadTrustMaterial(trustStore, null);
}
return HttpClients
.custom()
.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(new DefaultClientTlsStrategy(sslContextBuilder.build()))
.build())
.build();
}
@AllArgsConstructor
@Data
private static class HttpClientKey {
private final boolean https;
private final Certificate clientCertificate;
private final X509Certificate[] serverCertificates;
}
private static class HttpClientWrapper {
private final HttpClientKey key;
private final CloseableHttpClient closeableHttpClient;
private final AtomicLong pointers = new AtomicLong(1); // one use at create
private HttpClientWrapper(final HttpClientKey key, final CloseableHttpClient closeableHttpClient) {
this.key = key;
this.closeableHttpClient = closeableHttpClient;
}
private HttpClient get() {
pointers.incrementAndGet();
return closeableHttpClient;
}
private void release() {
if (pointers.decrementAndGet() <= 0) {
synchronized (HTTP_CLIENTS) {
HTTP_CLIENTS.remove(key);
}
try {
closeableHttpClient.close();
} catch (final IOException e) {
log.error("Failed to close http client", e);
}
}
}
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory == null ? null : trustManagerFactory.getTrustManagers(), null);
return new Client.Default(sslContext.getSocketFactory(), null);
}
}

View File

@@ -10,6 +10,7 @@
package org.eclipse.hawkbit.sdk;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import lombok.Data;
import lombok.ToString;
@@ -40,9 +41,9 @@ public class Tenant {
@Nullable
private String[] certificateFingerprints;
// the tenant DDI server certificate - it shall be trusted by controllers connecting via HTTPS
// the tenant DDI / Mgmt server certificates CA - it shall be trusted by controllers connecting via HTTPS
@Nullable
private Certificate ddiCertificate;
private X509Certificate[] tenantCA;
// Certificate Authority for the tenant that is used to sign the target certificates. It shall be trusted by the DDI server
@Nullable
private CA ddiCA;
@@ -51,8 +52,6 @@ public class Tenant {
@Nullable
private DMF dmf;
private boolean downloadAuthenticationEnabled = true;
@Data
@ToString
public static class DMF {

View File

@@ -45,9 +45,8 @@ public class DeviceApp {
@Bean
HawkbitClient hawkbitClient(
final HawkbitServer hawkBitServer,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
return new HawkbitClient(hawkBitServer, client, encoder, decoder, contract);
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
return new HawkbitClient(hawkBitServer, encoder, decoder, contract);
}
@Bean
@@ -88,7 +87,7 @@ public class DeviceApp {
@ShellMethod(key = "setup")
public void setup() {
mgmtApi.setupTargetAuthentication();
mgmtApi.setupTargetToken(device.getControllerId(), device.getTargetSecurityToken());
mgmtApi.setupTargetToken(device.getController().getControllerId(), device.getTargetSecurityToken());
}
@ShellMethod(key = "start")

View File

@@ -45,9 +45,8 @@ public class MultiDeviceApp {
@Bean
HawkbitClient hawkbitClient(
final HawkbitServer hawkBitServer,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
return new HawkbitClient(hawkBitServer, client, encoder, decoder, contract);
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
return new HawkbitClient(hawkBitServer, encoder, decoder, contract);
}
@Bean

View File

@@ -56,14 +56,12 @@ public class DdiController {
private static final String DEPLOYMENT_BASE_LINK = "deploymentBase";
private static final String CONFIRMATION_BASE_LINK = "confirmationBase";
private final String tenantId;
private final String controllerId;
private final Tenant tenant;
private final Controller controller;
private final UpdateHandler updateHandler;
private final DdiRootControllerRestApi ddiApi;
// configuration
private final boolean downloadAuthenticationEnabled;
private final String gatewayToken;
private final String targetSecurityToken;
private final Certificate certificate;
@@ -83,21 +81,25 @@ public class DdiController {
*
* @param tenant the tenant of the device belongs to
* @param controller the controller
* @param hawkbitClient a factory for creating to {@link DdiRootControllerRestApi} (and used)
* for communication to hawkBit
* @param hawkbitClient a factory for creating to {@link DdiRootControllerRestApi} (and used) for communication to hawkBit
*/
public DdiController(final Tenant tenant, final Controller controller,
final UpdateHandler updateHandler, final HawkbitClient hawkbitClient) {
this.tenantId = tenant.getTenantId();
gatewayToken = tenant.getGatewayToken();
downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled();
this.controllerId = controller.getControllerId();
public DdiController(final Tenant tenant, final Controller controller, final UpdateHandler updateHandler, final HawkbitClient hawkbitClient) {
this.tenant = tenant;
this.controller = controller;
this.targetSecurityToken = controller.getSecurityToken();
this.certificate = controller.getCertificate();
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
ddiApi = hawkbitClient.ddiService(DdiRootControllerRestApi.class, tenant, controller);
}
public String getTenantId() {
return tenant.getTenantId();
}
public String getControllerId() {
return controller.getControllerId();
}
// expects single threaded {@link java.util.concurrent.ScheduledExecutorService}
public void start(final ScheduledExecutorService executorService) {
stop();
@@ -146,7 +148,7 @@ public class DdiController {
}
private void poll() {
log.debug(LOG_PREFIX + " Polling ...", tenantId, controllerId);
log.debug(LOG_PREFIX + " Polling ...", getTenantId(), getControllerId());
Optional.ofNullable(executorService).ifPresent(executor ->
getControllerBase().ifPresentOrElse(
controllerBase -> {
@@ -176,8 +178,7 @@ public class DdiController {
final List<DdiChunk> modules = deployment.getChunks();
currentActionId = actionId;
executor.submit(
updateHandler.getUpdateProcessor(this, updateType, modules));
executor.submit(updateHandler.getUpdateProcessor(this, updateType, modules));
} else if (currentActionId != actionId) {
// TODO - cancel and start new one?
log.info(LOG_PREFIX + "Action {} is canceled while in process (new {})!", getTenantId(),

View File

@@ -13,8 +13,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
@@ -23,26 +21,17 @@ import java.util.HexFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.spi.ArtifactHandler;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Update handler provide plug-in endpoint allowing for customization of the update processing.
@@ -72,7 +61,6 @@ public interface UpdateHandler {
private static final String DOWNLOAD_LOG_MESSAGE = "Download ";
private static final String EXPECTED = "(Expected: ";
private static final String BUT_GOT_LOG_MESSAGE = " but got: ";
private static final int MINIMUM_TOKEN_LENGTH_FOR_HINT = 6;
private final DdiController ddiController;
private final DdiDeployment.HandlingType updateType;
private final List<DdiChunk> modules;
@@ -98,12 +86,8 @@ public interface UpdateHandler {
try {
final UpdateStatus updateStatus = download();
ddiController.sendFeedback(updateStatus);
if (updateStatus.status() == UpdateStatus.Status.FAILURE) {
return;
} else {
if (updateType != DdiDeployment.HandlingType.SKIP) {
ddiController.sendFeedback(update());
}
if (updateStatus.status() != UpdateStatus.Status.FAILURE && updateType != DdiDeployment.HandlingType.SKIP) {
ddiController.sendFeedback(update());
}
} finally {
cleanup();
@@ -133,15 +117,7 @@ public interface UpdateHandler {
log.info(LOG_PREFIX + "Start download", ddiController.getTenantId(), ddiController.getControllerId());
final List<UpdateStatus> updateStatusList = new ArrayList<>();
modules.forEach(module -> module.getArtifacts().forEach(artifact -> {
if (ddiController.isDownloadAuthenticationEnabled()) {
handleArtifact(
ddiController.getTargetSecurityToken(), ddiController.getGatewayToken(),
updateStatusList, artifact);
} else {
handleArtifact(null, null, updateStatusList, artifact);
}
}));
modules.forEach(module -> module.getArtifacts().forEach(artifact -> handleArtifact(updateStatusList, artifact)));
log.info(LOG_PREFIX + "Download complete.", ddiController.getTenantId(), ddiController.getControllerId());
@@ -180,39 +156,6 @@ public interface UpdateHandler {
log.debug(LOG_PREFIX + "Cleaned up", ddiController.getTenantId(), ddiController.getControllerId());
}
private static String hideTokenDetails(final String targetToken) {
if (targetToken == null) {
return "<NULL!>";
}
if (targetToken.isEmpty()) {
return "<EMPTY!>";
}
if (targetToken.length() <= MINIMUM_TOKEN_LENGTH_FOR_HINT) {
return "***";
}
return targetToken.substring(0, 2) + "***" + targetToken.substring(targetToken.length() - 2);
}
private static CloseableHttpClient createHttpClientThatAcceptsAllServerCerts()
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
return HttpClients
.custom()
.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(
new SSLConnectionSocketFactory(
SSLContextBuilder
.create()
.loadTrustMaterial(null, (chain, authType) -> true)
.build()))
.build()
)
.build();
}
private static Validator sizeValidator(final long size) {
return new Validator() {
@@ -288,106 +231,73 @@ public interface UpdateHandler {
};
}
private void handleArtifact(
final String targetToken, final String gatewayToken,
final List<UpdateStatus> status, final DdiArtifact artifact) {
private void handleArtifact(final List<UpdateStatus> status, final DdiArtifact artifact) {
artifact.getLink("download").ifPresentOrElse(
// HTTPS
link -> status.add(downloadUrl(link.getHref(), gatewayToken, targetToken,
artifact.getHashes(), artifact.getSize())),
link -> status.add(downloadUrl(link, artifact.getHashes(), artifact.getSize())),
// HTTP
() -> status.add(downloadUrl(
artifact.getLink("download-http")
.map(Link::getHref)
.orElseThrow(() -> new IllegalArgumentException("Nor https nor http found!")),
gatewayToken, targetToken,
artifact.getHashes(), artifact.getSize()))
);
}
private UpdateStatus downloadUrl(
final String url, final String gatewayToken, final String targetToken,
final DdiArtifactHash hash, final long size) {
private UpdateStatus downloadUrl(final Link link, final DdiArtifactHash hash, final long size) {
if (log.isDebugEnabled()) {
log.debug(LOG_PREFIX + "Downloading {} with token {}, expected hash {} and size {}",
ddiController.getTenantId(), ddiController.getControllerId(), url,
hideTokenDetails(targetToken), hash, size);
log.debug(LOG_PREFIX + "Downloading {}, expected hash {} and size {}",
ddiController.getTenantId(), ddiController.getControllerId(), link.getHref(), hash, size);
}
try {
return readAndCheckDownloadUrl(url, gatewayToken, targetToken, hash, size);
} catch (final IOException | KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
log.error(LOG_PREFIX + "Failed to download {}",
ddiController.getTenantId(), ddiController.getControllerId(), url, e);
return readAndCheckDownloadUrl(link, hash, size);
} catch (final NoSuchAlgorithmException | IOException e) {
log.error(LOG_PREFIX + "Failed to download {}", ddiController.getTenantId(), ddiController.getControllerId(), link.getHref(), e);
return new UpdateStatus(
UpdateStatus.Status.FAILURE,
List.of("Failed to download " + url + ": " + e.getMessage()));
List.of("Failed to download " + link.getHref() + ": " + e.getMessage()));
}
}
private UpdateStatus readAndCheckDownloadUrl(final String url, final String gatewayToken,
final String targetToken, final DdiArtifactHash hash, final long size)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
private UpdateStatus readAndCheckDownloadUrl(final Link link, final DdiArtifactHash hash, final long size)
throws NoSuchAlgorithmException, IOException {
final Validator sizeValidator = sizeValidator(size);
final Validator hashValidator = hashValidator(hash);
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(url);
try (final CloseableHttpClient httpclient = createHttpClientThatAcceptsAllServerCerts()) {
final HttpGet request = new HttpGet(url);
if (StringUtils.hasLength(targetToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "TargetToken " + targetToken);
} else if (StringUtils.hasLength(gatewayToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "GatewayToken " + gatewayToken);
}
return httpclient.execute(request, response -> {
try {
if (response.getCode() != HttpStatus.OK.value()) {
throw new IllegalStateException("Unexpected status code: " + response.getCode());
}
if (response.getEntity().getContentLength() != size) {
throw new IllegalArgumentException(
"Wrong content length " + EXPECTED + size + BUT_GOT_LOG_MESSAGE + response.getEntity()
.getContentLength() + ")!");
}
final byte[] buff = new byte[32 * 1024];
try (final InputStream is = response.getEntity().getContent()) {
for (int read; (read = is.read(buff)) != -1; ) {
sizeValidator.read(buff, read);
hashValidator.read(buff, read);
downloadHandler.read(buff, 0, read);
}
}
sizeValidator.validate();
hashValidator.validate();
final String message = "Downloaded " + url + " (" + size + " bytes)";
log.debug(LOG_PREFIX + message, ddiController.getTenantId(), ddiController.getControllerId());
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.SUCCESS);
downloadHandler.download().ifPresent(path -> downloads.put(url, path));
return new UpdateStatus(UpdateStatus.Status.SUCCESSFUL, List.of(message));
} catch (final Exception e) {
final String message = e.getMessage();
if (log.isTraceEnabled()) {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId(), e);
} else {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId());
}
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.ERROR);
return new UpdateStatus(UpdateStatus.Status.FAILURE, List.of(message));
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(link.getHref());
try (final InputStream is = HawkbitClient.getLink(link, InputStream.class, ddiController.getTenant(), ddiController.getController())) {
try {
final byte[] buff = new byte[32 * 1024];
for (int read; (read = is.read(buff)) != -1; ) {
sizeValidator.read(buff, read);
hashValidator.read(buff, read);
downloadHandler.read(buff, 0, read);
}
});
sizeValidator.validate();
hashValidator.validate();
final String message = "Downloaded " + link + " (" + size + " bytes)";
log.debug(LOG_PREFIX + message, ddiController.getTenantId(), ddiController.getControllerId());
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.SUCCESS);
downloadHandler.download().ifPresent(path -> downloads.put(link.getHref(), path));
return new UpdateStatus(UpdateStatus.Status.SUCCESSFUL, List.of(message));
} catch (final Exception e) {
final String message = e.getMessage();
if (log.isTraceEnabled()) {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + link + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId(), e);
} else {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + link + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId());
}
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.ERROR);
return new UpdateStatus(UpdateStatus.Status.FAILURE, List.of(message));
}
}
}
private interface Validator {
void read(final byte[] buff, final int len);
void validate();
}
}

View File

@@ -35,14 +35,11 @@ public class DmfController {
private static final String DEPLOYMENT_BASE_LINK = "deploymentBase";
private static final String CONFIRMATION_BASE_LINK = "confirmationBase";
private final String tenantId;
private final String controllerId;
private final Tenant tenant;
private final Controller controller;
private final UpdateHandler updateHandler;
private final DmfSender dmfSender;
// configuration
private final boolean downloadAuthenticationEnabled;
@Getter
private final Map<String, String> attributes = new HashMap<>();
@@ -64,13 +61,20 @@ public class DmfController {
final Tenant tenant, final Controller controller,
final UpdateHandler updateHandler,
final DmfSender dmfSender) {
this.tenantId = tenant.getTenantId();
downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled();
this.controllerId = controller.getControllerId();
this.tenant = tenant;
this.controller = controller;
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
this.dmfSender = dmfSender;
}
public String getTenantId() {
return tenant.getTenantId();
}
public String getControllerId() {
return controller.getControllerId();
}
public void start(ScheduledExecutorService executorService) {
stop();
this.executorService = executorService;
@@ -95,18 +99,18 @@ public class DmfController {
}
public void processUpdate(final EventTopic actionType, final DmfDownloadAndUpdateRequest updateRequest) {
log.info(LOG_PREFIX + "Processing update for action {} .", getTenantId(), controllerId, updateRequest.getActionId());
log.info(LOG_PREFIX + "Processing update for action {} .", getTenantId(), getControllerId(), updateRequest.getActionId());
executorService.submit(updateHandler.getUpdateProcessor(this, actionType, updateRequest));
}
public void sendFeedback(final UpdateStatus updateStatus) {
log.info(LOG_PREFIX + "Sending UPDATE_ACTION_STATUS for action : {}", getTenantId(), controllerId, currentActionId);
dmfSender.sendFeedback(tenantId, currentActionId, updateStatus);
log.info(LOG_PREFIX + "Sending UPDATE_ACTION_STATUS for action : {}", getTenantId(), getControllerId(), currentActionId);
dmfSender.sendFeedback(getTenantId(), currentActionId, updateStatus);
}
public void sendUpdateAttributes() {
log.info(LOG_PREFIX + "Sending UPDATE_ATTRIBUTES", getTenantId(), controllerId);
dmfSender.updateAttributes(tenantId, controllerId, DmfUpdateMode.MERGE, attributes);
log.info(LOG_PREFIX + "Sending UPDATE_ATTRIBUTES", getTenantId(), getControllerId());
dmfSender.updateAttributes(getTenantId(), getControllerId(), DmfUpdateMode.MERGE, attributes);
}
public void setAttribute(final String key, final String value) {

View File

@@ -13,8 +13,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
@@ -23,26 +21,20 @@ import java.util.HexFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
import org.eclipse.hawkbit.dmf.json.model.DmfArtifact;
import org.eclipse.hawkbit.dmf.json.model.DmfArtifactHash;
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.spi.ArtifactHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.hateoas.Link;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Update handler provide plug-in endpoint allowing for customization of the update processing.
@@ -134,20 +126,12 @@ public interface UpdateHandler {
" and hashes " + art.getHashes() + " ...")
.toList()));
log.info(LOG_PREFIX + "Start download", dmfController.getTenantId(), dmfController.getControllerId());
log.info(LOG_PREFIX + "Start download", dmfController.getTenant().getTenantId(), dmfController.getControllerId());
final List<UpdateStatus> updateStatusList = new ArrayList<>();
modules.forEach(module -> module.getArtifacts().forEach(artifact -> {
if (dmfController.isDownloadAuthenticationEnabled()) {
handleArtifact(
updateRequest.getTargetSecurityToken(),
updateStatusList, artifact);
} else {
handleArtifact(null, updateStatusList, artifact);
}
}));
modules.forEach(module -> module.getArtifacts().forEach(artifact -> handleArtifact(updateStatusList, artifact)));
log.info(LOG_PREFIX + "Download complete.", dmfController.getTenantId(), dmfController.getControllerId());
log.info(LOG_PREFIX + "Download complete.", dmfController.getTenant().getTenantId(), dmfController.getControllerId());
final List<String> messages = new LinkedList<>();
messages.add("Download complete.");
@@ -163,7 +147,7 @@ public interface UpdateHandler {
* may get the {@link #downloads} map and apply them
*/
protected UpdateStatus update() {
log.info(LOG_PREFIX + "Updated", dmfController.getTenantId(), dmfController.getControllerId());
log.info(LOG_PREFIX + "Updated", dmfController.getTenant().getTenantId(), dmfController.getControllerId());
return new UpdateStatus(DmfActionStatus.FINISHED, List.of("Update complete."));
}
@@ -177,11 +161,11 @@ public interface UpdateHandler {
Files.delete(path);
} catch (final IOException e) {
log.warn(LOG_PREFIX + "Failed to cleanup {}",
dmfController.getTenantId(), dmfController.getControllerId(),
dmfController.getTenant().getTenantId(), dmfController.getControllerId(),
path.toFile().getAbsolutePath(), e);
}
});
log.debug(LOG_PREFIX + "Cleaned up", dmfController.getTenantId(), dmfController.getControllerId());
log.debug(LOG_PREFIX + "Cleaned up", dmfController.getTenant().getTenantId(), dmfController.getControllerId());
}
private static String hideTokenDetails(final String targetToken) {
@@ -200,23 +184,6 @@ public interface UpdateHandler {
return targetToken.substring(0, 2) + "***" + targetToken.substring(targetToken.length() - 2);
}
private static CloseableHttpClient createHttpClientThatAcceptsAllServerCerts()
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
return HttpClients
.custom()
.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(
new SSLConnectionSocketFactory(
SSLContextBuilder
.create()
.loadTrustMaterial(null, (chain, authType) -> true)
.build()))
.build()
)
.build();
}
private static Validator sizeValidator(final long size) {
return new Validator() {
@@ -290,98 +257,70 @@ public interface UpdateHandler {
}
private void handleArtifact(
final String targetToken,
final List<UpdateStatus> status, final DmfArtifact artifact) {
if (artifact.getUrls().containsKey("HTTPS")) {
status.add(downloadUrl(artifact.getUrls().get("HTTPS"), targetToken,
artifact.getHashes(), artifact.getSize()));
status.add(downloadUrl(Link.of(artifact.getUrls().get("HTTPS")), artifact.getHashes(), artifact.getSize()));
} else if (artifact.getUrls().containsKey("HTTP")) {
status.add(downloadUrl(artifact.getUrls().get("HTTP"), targetToken,
artifact.getHashes(), artifact.getSize()));
status.add(downloadUrl(Link.of(artifact.getUrls().get("HTTP")), artifact.getHashes(), artifact.getSize()));
}
}
private UpdateStatus downloadUrl(
final String url, final String targetToken,
final DmfArtifactHash hash, final long size) {
private UpdateStatus downloadUrl(final Link link, final DmfArtifactHash hash, final long size) {
if (log.isDebugEnabled()) {
log.debug(LOG_PREFIX + "Downloading {} with token {}, expected hash {} and size {}",
dmfController.getTenantId(), dmfController.getControllerId(), url,
hideTokenDetails(targetToken), hash, size);
log.debug(LOG_PREFIX + "Downloading {}, expected hash {} and size {}",
dmfController.getTenant().getTenantId(), dmfController.getControllerId(), link.getHref(), hash, size);
}
try {
return readAndCheckDownloadUrl(url, targetToken, hash, size);
} catch (final IOException | KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
log.error(LOG_PREFIX + "Failed to download {}",
dmfController.getTenantId(), dmfController.getControllerId(), url, e);
return readAndCheckDownloadUrl(link, hash, size);
} catch (final NoSuchAlgorithmException | IOException e) {
log.error(LOG_PREFIX + "Failed to download {}", dmfController.getTenant().getTenantId(), dmfController.getControllerId(), link.getHref(), e);
return new UpdateStatus(
DmfActionStatus.ERROR,
List.of("Failed to download " + url + ": " + e.getMessage()));
List.of("Failed to download " + link.getHref() + ": " + e.getMessage()));
}
}
private UpdateStatus readAndCheckDownloadUrl(final String url,
final String targetToken, final DmfArtifactHash hash, final long size)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
private UpdateStatus readAndCheckDownloadUrl(final Link link, final DmfArtifactHash hash, final long size)
throws NoSuchAlgorithmException, IOException {
final Validator sizeValidator = sizeValidator(size);
final Validator hashValidator = hashValidator(hash);
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(url);
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(link.getHref());
try (final CloseableHttpClient httpclient = createHttpClientThatAcceptsAllServerCerts()) {
final HttpGet request = new HttpGet(url);
if (StringUtils.hasLength(targetToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "TargetToken " + targetToken);
}
return httpclient.execute(request, response -> {
try {
if (response.getCode() != HttpStatus.OK.value()) {
throw new IllegalStateException("Unexpected status code: " + response.getCode());
}
if (response.getEntity().getContentLength() != size) {
throw new IllegalArgumentException(
"Wrong content length " + EXPECTED + size + BUT_GOT_LOG_MESSAGE + response.getEntity()
.getContentLength() + ")!");
}
final byte[] buff = new byte[32 * 1024];
try (final InputStream is = response.getEntity().getContent()) {
for (int read; (read = is.read(buff)) != -1; ) {
sizeValidator.read(buff, read);
hashValidator.read(buff, read);
downloadHandler.read(buff, 0, read);
}
}
sizeValidator.validate();
hashValidator.validate();
final String message = "Downloaded " + url + " (" + size + " bytes)";
log.debug(LOG_PREFIX + message, dmfController.getTenantId(), dmfController.getControllerId());
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.SUCCESS);
downloadHandler.download().ifPresent(path -> downloads.put(url, path));
return new UpdateStatus(DmfActionStatus.FINISHED, List.of(message));
} catch (final Exception e) {
final String message = e.getMessage();
if (log.isTraceEnabled()) {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
dmfController.getTenantId(), dmfController.getControllerId(), e);
} else {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
dmfController.getTenantId(), dmfController.getControllerId());
}
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.ERROR);
return new UpdateStatus(DmfActionStatus.ERROR, List.of(message));
try (final InputStream is = HawkbitClient.getLink(link, InputStream.class, dmfController.getTenant(), dmfController.getController())) {
try {
final byte[] buff = new byte[32 * 1024];
for (int read; (read = is.read(buff)) != -1; ) {
sizeValidator.read(buff, read);
hashValidator.read(buff, read);
downloadHandler.read(buff, 0, read);
}
});
sizeValidator.validate();
hashValidator.validate();
final String message = "Downloaded " + link + " (" + size + " bytes)";
log.debug(LOG_PREFIX + message, dmfController.getTenant().getTenantId(), dmfController.getControllerId());
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.SUCCESS);
downloadHandler.download().ifPresent(path -> downloads.put(link.getHref(), path));
return new UpdateStatus(DmfActionStatus.FINISHED, List.of(message));
} catch (final Exception e) {
final String message = e.getMessage();
if (log.isTraceEnabled()) {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + link + " failed: " + message,
dmfController.getTenant().getTenantId(), dmfController.getControllerId(), e);
} else {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + link + " failed: " + message,
dmfController.getTenant().getTenantId(), dmfController.getControllerId());
}
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.ERROR);
return new UpdateStatus(DmfActionStatus.ERROR, List.of(message));
}
}
}
private interface Validator {
void read(final byte[] buff, final int len);
void validate();
}
}