diff --git a/hawkbit-autoconfigure/src/main/resources/hawkbitdefaults.properties b/hawkbit-autoconfigure/src/main/resources/hawkbitdefaults.properties index f5b6a392a..f859f6884 100644 --- a/hawkbit-autoconfigure/src/main/resources/hawkbitdefaults.properties +++ b/hawkbit-autoconfigure/src/main/resources/hawkbitdefaults.properties @@ -74,7 +74,7 @@ hawkbit.artifact.url.protocols.download-http.ip=127.0.0.1 hawkbit.artifact.url.protocols.download-http.protocol=http hawkbit.artifact.url.protocols.download-http.port=8080 hawkbit.artifact.url.protocols.download-http.supports=DMF,DDI -hawkbit.artifact.url.protocols.download-http.ref={protocol}://{hostname}:{port}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName} +hawkbit.artifact.url.protocols.download-http.ref={protocol}://{hostnameRequest}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName} hawkbit.artifact.url.protocols.md5sum-http.rel=md5sum-http hawkbit.artifact.url.protocols.md5sum-http.protocol=${hawkbit.artifact.url.protocols.download-http.protocol} hawkbit.artifact.url.protocols.md5sum-http.hostname=${hawkbit.artifact.url.protocols.download-http.hostname} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/ArtifactUrlHandler.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/ArtifactUrlHandler.java index 8f2036c82..7bcd42f1b 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/ArtifactUrlHandler.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/ArtifactUrlHandler.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.api; +import java.net.URI; import java.util.List; /** @@ -15,7 +16,6 @@ import java.util.List; * URLs to specific artifacts. * */ -@FunctionalInterface public interface ArtifactUrlHandler { /** @@ -30,4 +30,20 @@ public interface ArtifactUrlHandler { * @return an URL for the given artifact parameters in a given protocol */ List getUrls(URLPlaceholder placeholder, ApiType api); + + /** + * Returns a generated download URL for a given artifact parameters for a + * specific protocol. + * + * @param placeholder + * data for URL generation + * @param api + * given protocol that URL needs to support + * @param requestUri + * of the request that allows the handler to align the generated + * URL to the original request. + * + * @return an URL for the given artifact parameters in a given protocol + */ + List getUrls(URLPlaceholder placeholder, ApiType api, URI requestUri); } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandler.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandler.java index cb495d7fb..8c23bc3fa 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandler.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandler.java @@ -8,15 +8,19 @@ */ package org.eclipse.hawkbit.api; +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.eclipse.hawkbit.api.ArtifactUrlHandlerProperties.UrlProtocol; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.net.UrlEscapers; @@ -46,6 +50,9 @@ public class PropertyBasedArtifactUrlHandler implements ArtifactUrlHandler { private static final String IP_PLACEHOLDER = "ip"; private static final String PORT_PLACEHOLDER = "port"; private static final String HOSTNAME_PLACEHOLDER = "hostname"; + private static final String HOSTNAME_REQUEST_PLACEHOLDER = "hostnameRequest"; + private static final String PORT_REQUEST_PLACEHOLDER = "portRequest"; + private static final String HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER = "domainRequest"; private static final String ARTIFACT_FILENAME_PLACEHOLDER = "artifactFileName"; private static final String ARTIFACT_SHA1_PLACEHOLDER = "artifactSHA1"; private static final String ARTIFACT_ID_BASE10_PLACEHOLDER = "artifactId"; @@ -68,18 +75,24 @@ public class PropertyBasedArtifactUrlHandler implements ArtifactUrlHandler { @Override public List getUrls(final URLPlaceholder placeholder, final ApiType api) { + return getUrls(placeholder, api, null); + } + + @Override + public List getUrls(final URLPlaceholder placeholder, final ApiType api, final URI requestUri) { return urlHandlerProperties.getProtocols().entrySet().stream() .filter(entry -> entry.getValue().getSupports().contains(api)) .filter(entry -> entry.getValue().isEnabled()) .map(entry -> new ArtifactUrl(entry.getValue().getProtocol().toUpperCase(), entry.getValue().getRel(), - generateUrl(entry.getValue(), placeholder))) + generateUrl(entry.getValue(), placeholder, requestUri))) .collect(Collectors.toList()); } - private static String generateUrl(final UrlProtocol protocol, final URLPlaceholder placeholder) { - final Set> entrySet = getReplaceMap(protocol, placeholder).entrySet(); + private static String generateUrl(final UrlProtocol protocol, final URLPlaceholder placeholder, + final URI requestUri) { + final Set> entrySet = getReplaceMap(protocol, placeholder, requestUri).entrySet(); String urlPattern = protocol.getRef(); @@ -94,15 +107,21 @@ public class PropertyBasedArtifactUrlHandler implements ArtifactUrlHandler { return urlPattern; } - private static Map getReplaceMap(final UrlProtocol protocol, final URLPlaceholder placeholder) { + private static Map getReplaceMap(final UrlProtocol protocol, final URLPlaceholder placeholder, + final URI requestUri) { final Map replaceMap = new HashMap<>(); replaceMap.put(IP_PLACEHOLDER, protocol.getIp()); replaceMap.put(HOSTNAME_PLACEHOLDER, protocol.getHostname()); + + replaceMap.put(HOSTNAME_REQUEST_PLACEHOLDER, getRequestHost(protocol, requestUri)); + replaceMap.put(PORT_REQUEST_PLACEHOLDER, getRequestPort(protocol, requestUri)); + replaceMap.put(HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER, computeHostWithRequestDomain(protocol, requestUri)); + replaceMap.put(ARTIFACT_FILENAME_PLACEHOLDER, UrlEscapers.urlFragmentEscaper().escape(placeholder.getSoftwareData().getFilename())); replaceMap.put(ARTIFACT_SHA1_PLACEHOLDER, placeholder.getSoftwareData().getSha1Hash()); replaceMap.put(PROTOCOL_PLACEHOLDER, protocol.getProtocol()); - replaceMap.put(PORT_PLACEHOLDER, protocol.getPort() == null ? null : String.valueOf(protocol.getPort())); + replaceMap.put(PORT_PLACEHOLDER, getPort(protocol)); replaceMap.put(TENANT_PLACEHOLDER, placeholder.getTenant()); replaceMap.put(TENANT_ID_BASE10_PLACEHOLDER, String.valueOf(placeholder.getTenantId())); replaceMap.put(TENANT_ID_BASE62_PLACEHOLDER, Base62Util.fromBase10(placeholder.getTenantId())); @@ -119,4 +138,48 @@ public class PropertyBasedArtifactUrlHandler implements ArtifactUrlHandler { return replaceMap; } + private static String getRequestPort(final UrlProtocol protocol, final URI requestUri) { + if (requestUri == null) { + return getPort(protocol); + } + + return requestUri.getPort() > 0 ? String.valueOf(requestUri.getPort()) : getPort(protocol); + } + + private static String getRequestHost(final UrlProtocol protocol, final URI requestUri) { + if (requestUri == null) { + return protocol.getHostname(); + } + + return Optional.ofNullable(requestUri.getHost()).orElse(protocol.getHostname()); + } + + private static String getPort(final UrlProtocol protocol) { + return protocol.getPort() == null ? null : String.valueOf(protocol.getPort()); + } + + private static String computeHostWithRequestDomain(final UrlProtocol protocol, final URI requestUri) { + + if (requestUri == null) { + return protocol.getHostname(); + } + + if (!protocol.getHostname().contains(".")) { + return protocol.getHostname(); + } + + final String host = Splitter.on('.').trimResults().omitEmptyStrings().splitToList(protocol.getHostname()) + .get(0); + + final List domainElements = Splitter.on('.').trimResults().omitEmptyStrings() + .splitToList(requestUri.getHost()); + final String domain = Joiner.on(".").join(domainElements.subList(1, domainElements.size())); + + if (Strings.isNullOrEmpty(domain)) { + return protocol.getHostname(); + } + + return host + "." + domain; + } + } diff --git a/hawkbit-core/src/test/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandlerTest.java b/hawkbit-core/src/test/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandlerTest.java index 0a865a9ad..92ca11237 100644 --- a/hawkbit-core/src/test/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandlerTest.java +++ b/hawkbit-core/src/test/java/org/eclipse/hawkbit/api/PropertyBasedArtifactUrlHandlerTest.java @@ -11,6 +11,8 @@ package org.eclipse.hawkbit.api; import static org.fest.assertions.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; import org.eclipse.hawkbit.api.ArtifactUrlHandlerProperties.UrlProtocol; @@ -121,4 +123,72 @@ public class PropertyBasedArtifactUrlHandlerTest { TEST_PROTO + "://127.0.0.1:5683/fws/" + TENANT + "/" + TARGETID_BASE62 + "/" + ARTIFACTID_BASE62)), urls); } + + @Test + @Description("Verfies that the full qualified host of the statically defined hostname is replaced with the host of the request.") + public void urlGenerationWithHostFromRequest() throws URISyntaxException { + final String testHost = "ddi.host.com"; + + final UrlProtocol proto = new UrlProtocol(); + proto.setIp("127.0.0.1"); + proto.setPort(5683); + proto.setProtocol(TEST_PROTO); + proto.setRel(TEST_REL); + proto.setSupports(Lists.newArrayList(ApiType.DDI)); + proto.setRef("{protocol}://{hostnameRequest}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}"); + properties.getProtocols().put("ftp", proto); + + final List urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI, + new URI("https://" + testHost)); + + assertEquals(Lists.newArrayList(new ArtifactUrl(TEST_PROTO.toUpperCase(), TEST_REL, TEST_PROTO + "://" + + testHost + ":5683/fws/" + TENANT + "/" + TARGETID_BASE62 + "/" + ARTIFACTID_BASE62)), urls); + } + + @Test + @Description("Verfies that the port of the statically defined hostname is replaced with the port of the request.") + public void urlGenerationWithPortFromRequest() throws URISyntaxException { + final UrlProtocol proto = new UrlProtocol(); + proto.setRef( + "{protocol}://{hostname}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}"); + + properties.getProtocols().put("download-http", proto); + + final List ddiUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI, + new URI("http://anotherHost.com:8083")); + assertEquals(Lists.newArrayList(new ArtifactUrl("http".toUpperCase(), "download-http", + "http://localhost:8083/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/" + + SOFTWAREMODULEID + "/artifacts/" + FILENAME)), + ddiUrls); + + final List dmfUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DMF); + assertEquals(Lists.newArrayList(new ArtifactUrl("http".toUpperCase(), "download-http", + "http://localhost:8080/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/" + + SOFTWAREMODULEID + "/artifacts/" + FILENAME)), + dmfUrls); + } + + @Test + @Description("Verfies that the domain of the statically defined hostname is replaced with the domain of the request.") + public void urlGenerationWithDomainFromRequest() throws URISyntaxException { + final UrlProtocol proto = new UrlProtocol(); + proto.setHostname("host.bumlux.net"); + proto.setRef( + "{protocol}://{domainRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}"); + + properties.getProtocols().put("download-http", proto); + + final List ddiUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI, + new URI("http://anotherHost.com:8083")); + assertEquals(Lists.newArrayList( + new ArtifactUrl("http".toUpperCase(), "download-http", "http://host.com/" + TENANT + "/controller/v1/" + + CONTROLLER_ID + "/softwaremodules/" + SOFTWAREMODULEID + "/artifacts/" + FILENAME)), + ddiUrls); + + final List dmfUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DMF); + assertEquals(Lists.newArrayList(new ArtifactUrl("http".toUpperCase(), "download-http", + "http://host.bumlux.net/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/" + + SOFTWAREMODULEID + "/artifacts/" + FILENAME)), + dmfUrls); + } } diff --git a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java index 55da0792a..c650c26d3 100644 --- a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java +++ b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java @@ -36,6 +36,7 @@ import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.tenancy.TenantAware; import org.springframework.hateoas.Link; +import org.springframework.http.HttpRequest; import com.google.common.base.Charsets; @@ -49,10 +50,12 @@ public final class DataConversionHelper { } static List createChunks(final Target target, final Action uAction, - final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement) { + final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement, + final HttpRequest request) { return uAction.getDistributionSet().getModules().stream() .map(module -> new DdiChunk(mapChunkLegacyKeys(module.getType().getKey()), module.getVersion(), - module.getName(), createArtifacts(target, module, artifactUrlHandler, systemManagement))) + module.getName(), + createArtifacts(target, module, artifactUrlHandler, systemManagement, request))) .collect(Collectors.toList()); } @@ -68,30 +71,18 @@ public final class DataConversionHelper { return key; } - /** - * Creates all (rest) artifacts for a given software module. - * - * @param target - * for create URLs for - * @param module - * the software module - * @param artifactUrlHandler - * for creating download URLs - * @param systemManagement - * for access to tenant meta data - * @return a list of artifacts or a empty list. Cannot be . - */ - public static List createArtifacts(final Target target, + static List createArtifacts(final Target target, final org.eclipse.hawkbit.repository.model.SoftwareModule module, - final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement) { + final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement, + final HttpRequest request) { return module.getArtifacts().stream() - .map(artifact -> createArtifact(target, artifactUrlHandler, artifact, systemManagement)) + .map(artifact -> createArtifact(target, artifactUrlHandler, artifact, systemManagement, request)) .collect(Collectors.toList()); } private static DdiArtifact createArtifact(final Target target, final ArtifactUrlHandler artifactUrlHandler, - final Artifact artifact, final SystemManagement systemManagement) { + final Artifact artifact, final SystemManagement systemManagement, final HttpRequest request) { final DdiArtifact file = new DdiArtifact(); file.setHashes(new DdiArtifactHash(artifact.getSha1Hash(), artifact.getMd5Hash())); file.setFilename(artifact.getFilename()); @@ -102,7 +93,7 @@ public final class DataConversionHelper { systemManagement.getTenantMetadata().getId(), target.getControllerId(), target.getId(), new SoftwareData(artifact.getSoftwareModule().getId(), artifact.getFilename(), artifact.getId(), artifact.getSha1Hash())), - ApiType.DDI) + ApiType.DDI, request.getURI()) .forEach(entry -> file.add(new Link(entry.getRef()).withRel(entry.getRel()))); return file; diff --git a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java index 42491ff0a..dcc5173f0 100644 --- a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java +++ b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java @@ -55,6 +55,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -120,7 +121,8 @@ public class DdiRootController implements DdiRootControllerRestApi { } return new ResponseEntity<>( - DataConversionHelper.createArtifacts(target, softwareModule, artifactUrlHandler, systemManagement), + DataConversionHelper.createArtifacts(target, softwareModule, artifactUrlHandler, systemManagement, + new ServletServerHttpRequest(requestResponseContextHolder.getHttpServletRequest())), HttpStatus.OK); } @@ -244,7 +246,8 @@ public class DdiRootController implements DdiRootControllerRestApi { if (!action.isCancelingOrCanceled()) { final List chunks = DataConversionHelper.createChunks(target, action, artifactUrlHandler, - systemManagement); + systemManagement, + new ServletServerHttpRequest(requestResponseContextHolder.getHttpServletRequest())); final HandlingType handlingType = action.isForce() ? HandlingType.FORCED : HandlingType.ATTEMPT;