SDK: Add unified Link download (using auth contexts) (#2270)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -14,6 +14,8 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.ref.Cleaner;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
@@ -28,37 +30,50 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.UnrecoverableKeyException;
|
import java.security.UnrecoverableKeyException;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import feign.Client;
|
|
||||||
import feign.Contract;
|
import feign.Contract;
|
||||||
import feign.Feign;
|
import feign.Feign;
|
||||||
import feign.FeignException;
|
import feign.FeignException;
|
||||||
import feign.Request;
|
import feign.Request;
|
||||||
import feign.RequestInterceptor;
|
import feign.RequestInterceptor;
|
||||||
import feign.RequestTemplate;
|
import feign.RequestTemplate;
|
||||||
|
import feign.Response;
|
||||||
import feign.codec.Decoder;
|
import feign.codec.Decoder;
|
||||||
import feign.codec.Encoder;
|
import feign.codec.Encoder;
|
||||||
import feign.codec.ErrorDecoder;
|
import feign.codec.ErrorDecoder;
|
||||||
|
import feign.hc5.ApacheHttp5Client;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.HttpStatusCode;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
@@ -95,7 +110,6 @@ public class HawkbitClient {
|
|||||||
};
|
};
|
||||||
private final HawkbitServer hawkBitServer;
|
private final HawkbitServer hawkBitServer;
|
||||||
|
|
||||||
private final Client client;
|
|
||||||
private final Encoder encoder;
|
private final Encoder encoder;
|
||||||
private final Decoder decoder;
|
private final Decoder decoder;
|
||||||
private final Contract contract;
|
private final Contract contract;
|
||||||
@@ -104,21 +118,18 @@ public class HawkbitClient {
|
|||||||
private final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn;
|
private final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn;
|
||||||
|
|
||||||
public HawkbitClient(
|
public HawkbitClient(
|
||||||
final HawkbitServer hawkBitServer,
|
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
||||||
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
this(hawkBitServer, encoder, decoder, contract, null, null);
|
||||||
this(hawkBitServer, client, encoder, decoder, contract, null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customizers gets default ones and could
|
* Customizers gets default ones and could
|
||||||
*/
|
*/
|
||||||
public HawkbitClient(
|
public HawkbitClient(
|
||||||
final HawkbitServer hawkBitServer,
|
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract,
|
||||||
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract,
|
|
||||||
final ErrorDecoder errorDecoder,
|
final ErrorDecoder errorDecoder,
|
||||||
final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn) {
|
final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn) {
|
||||||
this.hawkBitServer = hawkBitServer;
|
this.hawkBitServer = hawkBitServer;
|
||||||
this.client = client;
|
|
||||||
this.encoder = encoder;
|
this.encoder = encoder;
|
||||||
this.decoder = decoder;
|
this.decoder = decoder;
|
||||||
this.contract = contract;
|
this.contract = contract;
|
||||||
@@ -135,6 +146,43 @@ public class HawkbitClient {
|
|||||||
return service(serviceType, tenantProperties, controller);
|
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) {
|
private <T> T service(final Class<T> serviceType, final Tenant tenant, final Controller controller) {
|
||||||
final T service = service0(serviceType, tenant, controller);
|
final T service = service0(serviceType, tenant, controller);
|
||||||
if (serviceType.isInterface() // proxy only interfaces
|
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) {
|
private <T> T service0(final Class<T> serviceType, final Tenant tenant, final Controller controller) {
|
||||||
final Client client;
|
final String url = controller == null ? hawkBitServer.getMgmtUrl() : hawkBitServer.getDdiUrl();
|
||||||
if (controller != null && controller.getCertificate() != null && hawkBitServer.getDdiUrl().startsWith("https://")) {
|
final HttpClientKey key = new HttpClientKey(
|
||||||
// mTLS could be requested
|
url.startsWith("https://"), controller == null ? null : controller.getCertificate(), tenant.getTenantCA());
|
||||||
try {
|
final HttpClient httpClient = httpClient(key);
|
||||||
client = mTlsClient(controller.getCertificate(), tenant);
|
final T service = Feign.builder()
|
||||||
} catch (final RuntimeException | Error e) {
|
.client(new ApacheHttp5Client(httpClient))
|
||||||
throw e;
|
|
||||||
} catch (final Exception e) {
|
|
||||||
throw new IllegalStateException("Failed to create mTLS client", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
client = this.client;
|
|
||||||
}
|
|
||||||
return Feign.builder().client(client)
|
|
||||||
.encoder(encoder)
|
.encoder(encoder)
|
||||||
.decoder(decoder)
|
.decoder(decoder)
|
||||||
.errorDecoder(errorDecoder)
|
.errorDecoder(errorDecoder)
|
||||||
.contract(contract)
|
.contract(contract)
|
||||||
.requestInterceptor(requestInterceptorFn.apply(tenant, controller))
|
.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")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -339,25 +386,95 @@ public class HawkbitClient {
|
|||||||
random.nextBytes(bytes);
|
random.nextBytes(bytes);
|
||||||
KEYSTORE_PASSWORD = Base64.getEncoder().encodeToString(bytes);
|
KEYSTORE_PASSWORD = Base64.getEncoder().encodeToString(bytes);
|
||||||
}
|
}
|
||||||
private static Client mTlsClient(final Certificate certificate, final Tenant tenant)
|
private static final Map<HttpClientKey, HttpClientWrapper> HTTP_CLIENTS = new HashMap<>();
|
||||||
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, CertificateException, IOException,
|
private static HttpClient httpClient(final HttpClientKey key) {
|
||||||
KeyManagementException {
|
synchronized (HTTP_CLIENTS) {
|
||||||
final KeyStore clientKeyStore = certificate.toKeyStore(KEYSTORE_PASSWORD);
|
final HttpClientWrapper httpClientWrapper = HTTP_CLIENTS.get(key);
|
||||||
final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
HttpClient client = httpClientWrapper == null ? null : httpClientWrapper.get();
|
||||||
keyManagerFactory.init(clientKeyStore, KEYSTORE_PASSWORD.toCharArray());
|
if (client == null) { // create
|
||||||
// Truststore
|
final CloseableHttpClient newClient;
|
||||||
final TrustManagerFactory trustManagerFactory;
|
if (key.isHttps()) {
|
||||||
if (tenant.getDdiCertificate() == null) {
|
// mTLS could be requested
|
||||||
trustManagerFactory = null;
|
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 {
|
} else {
|
||||||
final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
trustStore.load(null, null);
|
trustStore.load(null, null);
|
||||||
trustStore.setEntry("alias", new KeyStore.TrustedCertificateEntry(tenant.getDdiCertificate()), null);
|
for (int i = 0; i < serverCertificates.length; i++) {
|
||||||
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
trustStore.setEntry("alias" + i, new KeyStore.TrustedCertificateEntry(serverCertificates[i]), null);
|
||||||
trustManagerFactory.init(trustStore);
|
}
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
package org.eclipse.hawkbit.sdk;
|
package org.eclipse.hawkbit.sdk;
|
||||||
|
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
@@ -40,9 +41,9 @@ public class Tenant {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private String[] certificateFingerprints;
|
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
|
@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
|
// Certificate Authority for the tenant that is used to sign the target certificates. It shall be trusted by the DDI server
|
||||||
@Nullable
|
@Nullable
|
||||||
private CA ddiCA;
|
private CA ddiCA;
|
||||||
@@ -51,8 +52,6 @@ public class Tenant {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private DMF dmf;
|
private DMF dmf;
|
||||||
|
|
||||||
private boolean downloadAuthenticationEnabled = true;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ToString
|
@ToString
|
||||||
public static class DMF {
|
public static class DMF {
|
||||||
|
|||||||
@@ -45,9 +45,8 @@ public class DeviceApp {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
HawkbitClient hawkbitClient(
|
HawkbitClient hawkbitClient(
|
||||||
final HawkbitServer hawkBitServer,
|
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
||||||
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
return new HawkbitClient(hawkBitServer, encoder, decoder, contract);
|
||||||
return new HawkbitClient(hawkBitServer, client, encoder, decoder, contract);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -88,7 +87,7 @@ public class DeviceApp {
|
|||||||
@ShellMethod(key = "setup")
|
@ShellMethod(key = "setup")
|
||||||
public void setup() {
|
public void setup() {
|
||||||
mgmtApi.setupTargetAuthentication();
|
mgmtApi.setupTargetAuthentication();
|
||||||
mgmtApi.setupTargetToken(device.getControllerId(), device.getTargetSecurityToken());
|
mgmtApi.setupTargetToken(device.getController().getControllerId(), device.getTargetSecurityToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ShellMethod(key = "start")
|
@ShellMethod(key = "start")
|
||||||
|
|||||||
@@ -45,9 +45,8 @@ public class MultiDeviceApp {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
HawkbitClient hawkbitClient(
|
HawkbitClient hawkbitClient(
|
||||||
final HawkbitServer hawkBitServer,
|
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
||||||
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
return new HawkbitClient(hawkBitServer, encoder, decoder, contract);
|
||||||
return new HawkbitClient(hawkBitServer, client, encoder, decoder, contract);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -56,14 +56,12 @@ public class DdiController {
|
|||||||
private static final String DEPLOYMENT_BASE_LINK = "deploymentBase";
|
private static final String DEPLOYMENT_BASE_LINK = "deploymentBase";
|
||||||
private static final String CONFIRMATION_BASE_LINK = "confirmationBase";
|
private static final String CONFIRMATION_BASE_LINK = "confirmationBase";
|
||||||
|
|
||||||
private final String tenantId;
|
private final Tenant tenant;
|
||||||
private final String controllerId;
|
private final Controller controller;
|
||||||
private final UpdateHandler updateHandler;
|
private final UpdateHandler updateHandler;
|
||||||
private final DdiRootControllerRestApi ddiApi;
|
private final DdiRootControllerRestApi ddiApi;
|
||||||
|
|
||||||
// configuration
|
// configuration
|
||||||
private final boolean downloadAuthenticationEnabled;
|
|
||||||
private final String gatewayToken;
|
|
||||||
private final String targetSecurityToken;
|
private final String targetSecurityToken;
|
||||||
private final Certificate certificate;
|
private final Certificate certificate;
|
||||||
|
|
||||||
@@ -83,21 +81,25 @@ public class DdiController {
|
|||||||
*
|
*
|
||||||
* @param tenant the tenant of the device belongs to
|
* @param tenant the tenant of the device belongs to
|
||||||
* @param controller the controller
|
* @param controller the controller
|
||||||
* @param hawkbitClient a factory for creating to {@link DdiRootControllerRestApi} (and used)
|
* @param hawkbitClient a factory for creating to {@link DdiRootControllerRestApi} (and used) for communication to hawkBit
|
||||||
* for communication to hawkBit
|
|
||||||
*/
|
*/
|
||||||
public DdiController(final Tenant tenant, final Controller controller,
|
public DdiController(final Tenant tenant, final Controller controller, final UpdateHandler updateHandler, final HawkbitClient hawkbitClient) {
|
||||||
final UpdateHandler updateHandler, final HawkbitClient hawkbitClient) {
|
this.tenant = tenant;
|
||||||
this.tenantId = tenant.getTenantId();
|
this.controller = controller;
|
||||||
gatewayToken = tenant.getGatewayToken();
|
|
||||||
downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled();
|
|
||||||
this.controllerId = controller.getControllerId();
|
|
||||||
this.targetSecurityToken = controller.getSecurityToken();
|
this.targetSecurityToken = controller.getSecurityToken();
|
||||||
this.certificate = controller.getCertificate();
|
this.certificate = controller.getCertificate();
|
||||||
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
|
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
|
||||||
ddiApi = hawkbitClient.ddiService(DdiRootControllerRestApi.class, tenant, controller);
|
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}
|
// expects single threaded {@link java.util.concurrent.ScheduledExecutorService}
|
||||||
public void start(final ScheduledExecutorService executorService) {
|
public void start(final ScheduledExecutorService executorService) {
|
||||||
stop();
|
stop();
|
||||||
@@ -146,7 +148,7 @@ public class DdiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void poll() {
|
private void poll() {
|
||||||
log.debug(LOG_PREFIX + " Polling ...", tenantId, controllerId);
|
log.debug(LOG_PREFIX + " Polling ...", getTenantId(), getControllerId());
|
||||||
Optional.ofNullable(executorService).ifPresent(executor ->
|
Optional.ofNullable(executorService).ifPresent(executor ->
|
||||||
getControllerBase().ifPresentOrElse(
|
getControllerBase().ifPresentOrElse(
|
||||||
controllerBase -> {
|
controllerBase -> {
|
||||||
@@ -176,8 +178,7 @@ public class DdiController {
|
|||||||
final List<DdiChunk> modules = deployment.getChunks();
|
final List<DdiChunk> modules = deployment.getChunks();
|
||||||
|
|
||||||
currentActionId = actionId;
|
currentActionId = actionId;
|
||||||
executor.submit(
|
executor.submit(updateHandler.getUpdateProcessor(this, updateType, modules));
|
||||||
updateHandler.getUpdateProcessor(this, updateType, modules));
|
|
||||||
} else if (currentActionId != actionId) {
|
} else if (currentActionId != actionId) {
|
||||||
// TODO - cancel and start new one?
|
// TODO - cancel and start new one?
|
||||||
log.info(LOG_PREFIX + "Action {} is canceled while in process (new {})!", getTenantId(),
|
log.info(LOG_PREFIX + "Action {} is canceled while in process (new {})!", getTenantId(),
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -23,26 +21,17 @@ import java.util.HexFormat;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.DdiArtifact;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment;
|
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment;
|
||||||
|
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||||
import org.eclipse.hawkbit.sdk.spi.ArtifactHandler;
|
import org.eclipse.hawkbit.sdk.spi.ArtifactHandler;
|
||||||
import org.springframework.hateoas.Link;
|
import org.springframework.hateoas.Link;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update handler provide plug-in endpoint allowing for customization of the update processing.
|
* 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 DOWNLOAD_LOG_MESSAGE = "Download ";
|
||||||
private static final String EXPECTED = "(Expected: ";
|
private static final String EXPECTED = "(Expected: ";
|
||||||
private static final String BUT_GOT_LOG_MESSAGE = " but got: ";
|
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 DdiController ddiController;
|
||||||
private final DdiDeployment.HandlingType updateType;
|
private final DdiDeployment.HandlingType updateType;
|
||||||
private final List<DdiChunk> modules;
|
private final List<DdiChunk> modules;
|
||||||
@@ -98,12 +86,8 @@ public interface UpdateHandler {
|
|||||||
try {
|
try {
|
||||||
final UpdateStatus updateStatus = download();
|
final UpdateStatus updateStatus = download();
|
||||||
ddiController.sendFeedback(updateStatus);
|
ddiController.sendFeedback(updateStatus);
|
||||||
if (updateStatus.status() == UpdateStatus.Status.FAILURE) {
|
if (updateStatus.status() != UpdateStatus.Status.FAILURE && updateType != DdiDeployment.HandlingType.SKIP) {
|
||||||
return;
|
ddiController.sendFeedback(update());
|
||||||
} else {
|
|
||||||
if (updateType != DdiDeployment.HandlingType.SKIP) {
|
|
||||||
ddiController.sendFeedback(update());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -133,15 +117,7 @@ public interface UpdateHandler {
|
|||||||
log.info(LOG_PREFIX + "Start download", ddiController.getTenantId(), ddiController.getControllerId());
|
log.info(LOG_PREFIX + "Start download", ddiController.getTenantId(), ddiController.getControllerId());
|
||||||
|
|
||||||
final List<UpdateStatus> updateStatusList = new ArrayList<>();
|
final List<UpdateStatus> updateStatusList = new ArrayList<>();
|
||||||
modules.forEach(module -> module.getArtifacts().forEach(artifact -> {
|
modules.forEach(module -> module.getArtifacts().forEach(artifact -> handleArtifact(updateStatusList, artifact)));
|
||||||
if (ddiController.isDownloadAuthenticationEnabled()) {
|
|
||||||
handleArtifact(
|
|
||||||
ddiController.getTargetSecurityToken(), ddiController.getGatewayToken(),
|
|
||||||
updateStatusList, artifact);
|
|
||||||
} else {
|
|
||||||
handleArtifact(null, null, updateStatusList, artifact);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
log.info(LOG_PREFIX + "Download complete.", ddiController.getTenantId(), ddiController.getControllerId());
|
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());
|
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) {
|
private static Validator sizeValidator(final long size) {
|
||||||
return new Validator() {
|
return new Validator() {
|
||||||
|
|
||||||
@@ -288,106 +231,73 @@ public interface UpdateHandler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleArtifact(
|
private void handleArtifact(final List<UpdateStatus> status, final DdiArtifact artifact) {
|
||||||
final String targetToken, final String gatewayToken,
|
|
||||||
final List<UpdateStatus> status, final DdiArtifact artifact) {
|
|
||||||
artifact.getLink("download").ifPresentOrElse(
|
artifact.getLink("download").ifPresentOrElse(
|
||||||
// HTTPS
|
// HTTPS
|
||||||
link -> status.add(downloadUrl(link.getHref(), gatewayToken, targetToken,
|
link -> status.add(downloadUrl(link, artifact.getHashes(), artifact.getSize())),
|
||||||
artifact.getHashes(), artifact.getSize())),
|
|
||||||
// HTTP
|
// HTTP
|
||||||
() -> status.add(downloadUrl(
|
() -> status.add(downloadUrl(
|
||||||
artifact.getLink("download-http")
|
artifact.getLink("download-http")
|
||||||
.map(Link::getHref)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Nor https nor http found!")),
|
.orElseThrow(() -> new IllegalArgumentException("Nor https nor http found!")),
|
||||||
gatewayToken, targetToken,
|
|
||||||
artifact.getHashes(), artifact.getSize()))
|
artifact.getHashes(), artifact.getSize()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UpdateStatus downloadUrl(
|
private UpdateStatus downloadUrl(final Link link, final DdiArtifactHash hash, final long size) {
|
||||||
final String url, final String gatewayToken, final String targetToken,
|
|
||||||
final DdiArtifactHash hash, final long size) {
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug(LOG_PREFIX + "Downloading {} with token {}, expected hash {} and size {}",
|
log.debug(LOG_PREFIX + "Downloading {}, expected hash {} and size {}",
|
||||||
ddiController.getTenantId(), ddiController.getControllerId(), url,
|
ddiController.getTenantId(), ddiController.getControllerId(), link.getHref(), hash, size);
|
||||||
hideTokenDetails(targetToken), hash, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return readAndCheckDownloadUrl(url, gatewayToken, targetToken, hash, size);
|
return readAndCheckDownloadUrl(link, hash, size);
|
||||||
} catch (final IOException | KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
} catch (final NoSuchAlgorithmException | IOException e) {
|
||||||
log.error(LOG_PREFIX + "Failed to download {}",
|
log.error(LOG_PREFIX + "Failed to download {}", ddiController.getTenantId(), ddiController.getControllerId(), link.getHref(), e);
|
||||||
ddiController.getTenantId(), ddiController.getControllerId(), url, e);
|
|
||||||
return new UpdateStatus(
|
return new UpdateStatus(
|
||||||
UpdateStatus.Status.FAILURE,
|
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,
|
private UpdateStatus readAndCheckDownloadUrl(final Link link, final DdiArtifactHash hash, final long size)
|
||||||
final String targetToken, final DdiArtifactHash hash, final long size)
|
throws NoSuchAlgorithmException, IOException {
|
||||||
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
|
|
||||||
final Validator sizeValidator = sizeValidator(size);
|
final Validator sizeValidator = sizeValidator(size);
|
||||||
final Validator hashValidator = hashValidator(hash);
|
final Validator hashValidator = hashValidator(hash);
|
||||||
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(url);
|
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(link.getHref());
|
||||||
|
try (final InputStream is = HawkbitClient.getLink(link, InputStream.class, ddiController.getTenant(), ddiController.getController())) {
|
||||||
try (final CloseableHttpClient httpclient = createHttpClientThatAcceptsAllServerCerts()) {
|
try {
|
||||||
final HttpGet request = new HttpGet(url);
|
final byte[] buff = new byte[32 * 1024];
|
||||||
if (StringUtils.hasLength(targetToken)) {
|
for (int read; (read = is.read(buff)) != -1; ) {
|
||||||
request.addHeader(HttpHeaders.AUTHORIZATION, "TargetToken " + targetToken);
|
sizeValidator.read(buff, read);
|
||||||
} else if (StringUtils.hasLength(gatewayToken)) {
|
hashValidator.read(buff, read);
|
||||||
request.addHeader(HttpHeaders.AUTHORIZATION, "GatewayToken " + gatewayToken);
|
downloadHandler.read(buff, 0, read);
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
});
|
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 {
|
private interface Validator {
|
||||||
|
|
||||||
void read(final byte[] buff, final int len);
|
void read(final byte[] buff, final int len);
|
||||||
|
|
||||||
void validate();
|
void validate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,11 @@ public class DmfController {
|
|||||||
private static final String DEPLOYMENT_BASE_LINK = "deploymentBase";
|
private static final String DEPLOYMENT_BASE_LINK = "deploymentBase";
|
||||||
private static final String CONFIRMATION_BASE_LINK = "confirmationBase";
|
private static final String CONFIRMATION_BASE_LINK = "confirmationBase";
|
||||||
|
|
||||||
private final String tenantId;
|
private final Tenant tenant;
|
||||||
private final String controllerId;
|
private final Controller controller;
|
||||||
private final UpdateHandler updateHandler;
|
private final UpdateHandler updateHandler;
|
||||||
private final DmfSender dmfSender;
|
private final DmfSender dmfSender;
|
||||||
|
|
||||||
// configuration
|
|
||||||
private final boolean downloadAuthenticationEnabled;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Map<String, String> attributes = new HashMap<>();
|
private final Map<String, String> attributes = new HashMap<>();
|
||||||
|
|
||||||
@@ -64,13 +61,20 @@ public class DmfController {
|
|||||||
final Tenant tenant, final Controller controller,
|
final Tenant tenant, final Controller controller,
|
||||||
final UpdateHandler updateHandler,
|
final UpdateHandler updateHandler,
|
||||||
final DmfSender dmfSender) {
|
final DmfSender dmfSender) {
|
||||||
this.tenantId = tenant.getTenantId();
|
this.tenant = tenant;
|
||||||
downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled();
|
this.controller = controller;
|
||||||
this.controllerId = controller.getControllerId();
|
|
||||||
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
|
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
|
||||||
this.dmfSender = dmfSender;
|
this.dmfSender = dmfSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTenantId() {
|
||||||
|
return tenant.getTenantId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getControllerId() {
|
||||||
|
return controller.getControllerId();
|
||||||
|
}
|
||||||
|
|
||||||
public void start(ScheduledExecutorService executorService) {
|
public void start(ScheduledExecutorService executorService) {
|
||||||
stop();
|
stop();
|
||||||
this.executorService = executorService;
|
this.executorService = executorService;
|
||||||
@@ -95,18 +99,18 @@ public class DmfController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void processUpdate(final EventTopic actionType, final DmfDownloadAndUpdateRequest updateRequest) {
|
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));
|
executorService.submit(updateHandler.getUpdateProcessor(this, actionType, updateRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendFeedback(final UpdateStatus updateStatus) {
|
public void sendFeedback(final UpdateStatus updateStatus) {
|
||||||
log.info(LOG_PREFIX + "Sending UPDATE_ACTION_STATUS for action : {}", getTenantId(), controllerId, currentActionId);
|
log.info(LOG_PREFIX + "Sending UPDATE_ACTION_STATUS for action : {}", getTenantId(), getControllerId(), currentActionId);
|
||||||
dmfSender.sendFeedback(tenantId, currentActionId, updateStatus);
|
dmfSender.sendFeedback(getTenantId(), currentActionId, updateStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendUpdateAttributes() {
|
public void sendUpdateAttributes() {
|
||||||
log.info(LOG_PREFIX + "Sending UPDATE_ATTRIBUTES", getTenantId(), controllerId);
|
log.info(LOG_PREFIX + "Sending UPDATE_ATTRIBUTES", getTenantId(), getControllerId());
|
||||||
dmfSender.updateAttributes(tenantId, controllerId, DmfUpdateMode.MERGE, attributes);
|
dmfSender.updateAttributes(getTenantId(), getControllerId(), DmfUpdateMode.MERGE, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAttribute(final String key, final String value) {
|
public void setAttribute(final String key, final String value) {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -23,26 +21,20 @@ import java.util.HexFormat;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.amqp.api.EventTopic;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
|
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfArtifact;
|
import org.eclipse.hawkbit.dmf.json.model.DmfArtifact;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfArtifactHash;
|
import org.eclipse.hawkbit.dmf.json.model.DmfArtifactHash;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
|
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule;
|
import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule;
|
||||||
|
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||||
import org.eclipse.hawkbit.sdk.spi.ArtifactHandler;
|
import org.eclipse.hawkbit.sdk.spi.ArtifactHandler;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.hateoas.Link;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update handler provide plug-in endpoint allowing for customization of the update processing.
|
* Update handler provide plug-in endpoint allowing for customization of the update processing.
|
||||||
@@ -134,20 +126,12 @@ public interface UpdateHandler {
|
|||||||
" and hashes " + art.getHashes() + " ...")
|
" and hashes " + art.getHashes() + " ...")
|
||||||
.toList()));
|
.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<>();
|
final List<UpdateStatus> updateStatusList = new ArrayList<>();
|
||||||
modules.forEach(module -> module.getArtifacts().forEach(artifact -> {
|
modules.forEach(module -> module.getArtifacts().forEach(artifact -> handleArtifact(updateStatusList, artifact)));
|
||||||
if (dmfController.isDownloadAuthenticationEnabled()) {
|
|
||||||
handleArtifact(
|
|
||||||
updateRequest.getTargetSecurityToken(),
|
|
||||||
updateStatusList, artifact);
|
|
||||||
} else {
|
|
||||||
handleArtifact(null, 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<>();
|
final List<String> messages = new LinkedList<>();
|
||||||
messages.add("Download complete.");
|
messages.add("Download complete.");
|
||||||
@@ -163,7 +147,7 @@ public interface UpdateHandler {
|
|||||||
* may get the {@link #downloads} map and apply them
|
* may get the {@link #downloads} map and apply them
|
||||||
*/
|
*/
|
||||||
protected UpdateStatus update() {
|
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."));
|
return new UpdateStatus(DmfActionStatus.FINISHED, List.of("Update complete."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,11 +161,11 @@ public interface UpdateHandler {
|
|||||||
Files.delete(path);
|
Files.delete(path);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
log.warn(LOG_PREFIX + "Failed to cleanup {}",
|
log.warn(LOG_PREFIX + "Failed to cleanup {}",
|
||||||
dmfController.getTenantId(), dmfController.getControllerId(),
|
dmfController.getTenant().getTenantId(), dmfController.getControllerId(),
|
||||||
path.toFile().getAbsolutePath(), e);
|
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) {
|
private static String hideTokenDetails(final String targetToken) {
|
||||||
@@ -200,23 +184,6 @@ public interface UpdateHandler {
|
|||||||
return targetToken.substring(0, 2) + "***" + targetToken.substring(targetToken.length() - 2);
|
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) {
|
private static Validator sizeValidator(final long size) {
|
||||||
return new Validator() {
|
return new Validator() {
|
||||||
|
|
||||||
@@ -290,98 +257,70 @@ public interface UpdateHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleArtifact(
|
private void handleArtifact(
|
||||||
final String targetToken,
|
|
||||||
final List<UpdateStatus> status, final DmfArtifact artifact) {
|
final List<UpdateStatus> status, final DmfArtifact artifact) {
|
||||||
if (artifact.getUrls().containsKey("HTTPS")) {
|
if (artifact.getUrls().containsKey("HTTPS")) {
|
||||||
status.add(downloadUrl(artifact.getUrls().get("HTTPS"), targetToken,
|
status.add(downloadUrl(Link.of(artifact.getUrls().get("HTTPS")), artifact.getHashes(), artifact.getSize()));
|
||||||
artifact.getHashes(), artifact.getSize()));
|
|
||||||
} else if (artifact.getUrls().containsKey("HTTP")) {
|
} else if (artifact.getUrls().containsKey("HTTP")) {
|
||||||
status.add(downloadUrl(artifact.getUrls().get("HTTP"), targetToken,
|
status.add(downloadUrl(Link.of(artifact.getUrls().get("HTTP")), artifact.getHashes(), artifact.getSize()));
|
||||||
artifact.getHashes(), artifact.getSize()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private UpdateStatus downloadUrl(
|
private UpdateStatus downloadUrl(final Link link, final DmfArtifactHash hash, final long size) {
|
||||||
final String url, final String targetToken,
|
|
||||||
final DmfArtifactHash hash, final long size) {
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug(LOG_PREFIX + "Downloading {} with token {}, expected hash {} and size {}",
|
log.debug(LOG_PREFIX + "Downloading {}, expected hash {} and size {}",
|
||||||
dmfController.getTenantId(), dmfController.getControllerId(), url,
|
dmfController.getTenant().getTenantId(), dmfController.getControllerId(), link.getHref(), hash, size);
|
||||||
hideTokenDetails(targetToken), hash, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return readAndCheckDownloadUrl(url, targetToken, hash, size);
|
return readAndCheckDownloadUrl(link, hash, size);
|
||||||
} catch (final IOException | KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
} catch (final NoSuchAlgorithmException | IOException e) {
|
||||||
log.error(LOG_PREFIX + "Failed to download {}",
|
log.error(LOG_PREFIX + "Failed to download {}", dmfController.getTenant().getTenantId(), dmfController.getControllerId(), link.getHref(), e);
|
||||||
dmfController.getTenantId(), dmfController.getControllerId(), url, e);
|
|
||||||
return new UpdateStatus(
|
return new UpdateStatus(
|
||||||
DmfActionStatus.ERROR,
|
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,
|
private UpdateStatus readAndCheckDownloadUrl(final Link link, final DmfArtifactHash hash, final long size)
|
||||||
final String targetToken, final DmfArtifactHash hash, final long size)
|
throws NoSuchAlgorithmException, IOException {
|
||||||
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
|
|
||||||
final Validator sizeValidator = sizeValidator(size);
|
final Validator sizeValidator = sizeValidator(size);
|
||||||
final Validator hashValidator = hashValidator(hash);
|
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()) {
|
try (final InputStream is = HawkbitClient.getLink(link, InputStream.class, dmfController.getTenant(), dmfController.getController())) {
|
||||||
final HttpGet request = new HttpGet(url);
|
try {
|
||||||
if (StringUtils.hasLength(targetToken)) {
|
final byte[] buff = new byte[32 * 1024];
|
||||||
request.addHeader(HttpHeaders.AUTHORIZATION, "TargetToken " + targetToken);
|
for (int read; (read = is.read(buff)) != -1; ) {
|
||||||
}
|
sizeValidator.read(buff, read);
|
||||||
|
hashValidator.read(buff, read);
|
||||||
return httpclient.execute(request, response -> {
|
downloadHandler.read(buff, 0, read);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
});
|
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 {
|
private interface Validator {
|
||||||
|
|
||||||
void read(final byte[] buff, final int len);
|
void read(final byte[] buff, final int len);
|
||||||
|
|
||||||
void validate();
|
void validate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import com.vaadin.flow.component.page.AppShellConfigurator;
|
|||||||
import com.vaadin.flow.server.PWA;
|
import com.vaadin.flow.server.PWA;
|
||||||
import com.vaadin.flow.theme.Theme;
|
import com.vaadin.flow.theme.Theme;
|
||||||
import com.vaadin.flow.theme.lumo.Lumo;
|
import com.vaadin.flow.theme.lumo.Lumo;
|
||||||
import feign.Client;
|
|
||||||
import feign.Contract;
|
import feign.Contract;
|
||||||
import feign.RequestInterceptor;
|
import feign.RequestInterceptor;
|
||||||
import feign.codec.Decoder;
|
import feign.codec.Decoder;
|
||||||
@@ -68,10 +67,9 @@ public class SimpleUIApp implements AppShellConfigurator {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
HawkbitClient hawkbitClient(
|
HawkbitClient hawkbitClient(
|
||||||
final HawkbitServer hawkBitServer,
|
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
||||||
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
|
||||||
return new HawkbitClient(
|
return new HawkbitClient(
|
||||||
hawkBitServer, client, encoder, decoder, contract,
|
hawkBitServer, encoder, decoder, contract,
|
||||||
ERROR_DECODER,
|
ERROR_DECODER,
|
||||||
(tenant, controller) ->
|
(tenant, controller) ->
|
||||||
controller == null ?
|
controller == null ?
|
||||||
|
|||||||
Reference in New Issue
Block a user