diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java index e883dbc315..d8d6c2e338 100644 --- a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java @@ -14,6 +14,8 @@ 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.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 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 requestInterceptorFn) { this.hawkBitServer = hawkBitServer; - this.client = client; this.encoder = encoder; this.decoder = decoder; this.contract = contract; @@ -135,6 +146,43 @@ public T ddiService(final Class serviceType, final Tenant tenantPropertie return service(serviceType, tenantProperties, controller); } + public static T getLink(final Link link, final Class 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 service(final Class serviceType, final Tenant tenant, final Controller controller) { final T service = service0(serviceType, tenant, controller); if (serviceType.isInterface() // proxy only interfaces @@ -149,27 +197,26 @@ private T service(final Class serviceType, final Tenant tenant, final Con } } + private static final Cleaner CLEANER = Cleaner.create(); private T service0(final Class 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 @@ private static T getAnnotation(final Class annotationC 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 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); } } \ No newline at end of file diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java index 550480a65b..9352e22290 100644 --- a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java @@ -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 { diff --git a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java index 65f3d8b14c..734b731cd7 100644 --- a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java +++ b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java @@ -45,9 +45,8 @@ public static void main(String[] args) { @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 static class Shell { @ShellMethod(key = "setup") public void setup() { mgmtApi.setupTargetAuthentication(); - mgmtApi.setupTargetToken(device.getControllerId(), device.getTargetSecurityToken()); + mgmtApi.setupTargetToken(device.getController().getControllerId(), device.getTargetSecurityToken()); } @ShellMethod(key = "start") diff --git a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java index b4d7567b8f..d5f7f6299d 100644 --- a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java +++ b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java @@ -45,9 +45,8 @@ public static void main(String[] args) { @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 diff --git a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java index ea52dc17ac..6c40529eb3 100644 --- a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java +++ b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java @@ -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 void sendFeedback(final UpdateStatus updateStatus) { } 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 @@ private void poll() { final List 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(), diff --git a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/UpdateHandler.java b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/UpdateHandler.java index 8ab237cdf2..ffe5b9f5e8 100644 --- a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/UpdateHandler.java +++ b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/UpdateHandler.java @@ -13,8 +13,6 @@ 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.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 @@ class UpdateProcessor implements Runnable { 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 modules; @@ -98,12 +86,8 @@ public void run() { 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 @@ protected UpdateStatus download() { log.info(LOG_PREFIX + "Start download", ddiController.getTenantId(), ddiController.getControllerId()); final List 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 @@ protected void cleanup() { log.debug(LOG_PREFIX + "Cleaned up", ddiController.getTenantId(), ddiController.getControllerId()); } - private static String hideTokenDetails(final String targetToken) { - if (targetToken == null) { - return ""; - } - - if (targetToken.isEmpty()) { - return ""; - } - - 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 void validate() { }; } - private void handleArtifact( - final String targetToken, final String gatewayToken, - final List status, final DdiArtifact artifact) { + private void handleArtifact(final List 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(); } } diff --git a/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/DmfController.java b/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/DmfController.java index 8f0a69eedb..5400c20a5b 100644 --- a/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/DmfController.java +++ b/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/DmfController.java @@ -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 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 void stop() { } 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) { diff --git a/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/UpdateHandler.java b/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/UpdateHandler.java index f392be5c3c..df65e7cf05 100644 --- a/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/UpdateHandler.java +++ b/hawkbit-sdk/hawkbit-sdk-dmf/src/main/java/org/eclipse/hawkbit/sdk/dmf/UpdateHandler.java @@ -13,8 +13,6 @@ 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.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 @@ protected UpdateStatus download() { " 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 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 messages = new LinkedList<>(); messages.add("Download complete."); @@ -163,7 +147,7 @@ protected UpdateStatus download() { * 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 @@ protected void cleanup() { 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 @@ private static String hideTokenDetails(final String targetToken) { 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 void validate() { } private void handleArtifact( - final String targetToken, final List 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); - - 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 ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(link.getHref()); - 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(); } } diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java index a956d181ff..4c9fb81acf 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java @@ -20,7 +20,6 @@ import com.vaadin.flow.server.PWA; import com.vaadin.flow.theme.Theme; import com.vaadin.flow.theme.lumo.Lumo; -import feign.Client; import feign.Contract; import feign.RequestInterceptor; import feign.codec.Decoder; @@ -68,10 +67,9 @@ public static void main(String[] args) { @Bean HawkbitClient 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) { return new HawkbitClient( - hawkBitServer, client, encoder, decoder, contract, + hawkBitServer, encoder, decoder, contract, ERROR_DECODER, (tenant, controller) -> controller == null ?