From 1ee3d0c850d1ca1b1bc76243f3317d703b5efbef Mon Sep 17 00:00:00 2001 From: Kai Zimmermann Date: Wed, 3 May 2017 09:21:14 +0200 Subject: [PATCH] Remove module for legacy download API (never used by hawkBit). (#500) * Remove module for legacy download API (never used by hawkBit). Signed-off-by: kaizimmerm * Add stream aware error controller. Signed-off-by: kaizimmerm * Fixed bean definition. Signed-off-by: kaizimmerm --- .../SecurityManagedConfiguration.java | 7 +- hawkbit-ddi-dl-api/README.md | 16 - hawkbit-ddi-dl-api/pom.xml | 28 -- .../DdiDlArtifactStoreControllerRestApi.java | 63 ---- .../ddi/dl/rest/api/DdiDlRestConstants.java | 40 --- hawkbit-ddi-resource/pom.xml | 5 - .../rest/resource/DataConversionHelper.java | 3 +- .../resource/DdiArtifactStoreController.java | 156 ---------- .../resource/DdiArtifactDownloadTest.java | 285 +++--------------- .../test/util/AbstractIntegrationTest.java | 4 +- .../app/StreamAwareErrorController.java | 49 +++ pom.xml | 1 - 12 files changed, 97 insertions(+), 560 deletions(-) delete mode 100644 hawkbit-ddi-dl-api/README.md delete mode 100644 hawkbit-ddi-dl-api/pom.xml delete mode 100644 hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlArtifactStoreControllerRestApi.java delete mode 100644 hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlRestConstants.java delete mode 100644 hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java create mode 100644 hawkbit-runtime/hawkbit-update-server/src/main/java/org/eclipse/hawkbit/app/StreamAwareErrorController.java diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java index bc7cf5284..e6d4ccc5f 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -289,9 +289,10 @@ public class SecurityManagedConfiguration { // in the ShallowEtagHeaderFilter, just using the SH1 hash of the // artifact itself as 'ETag', because otherwise the file will be copied // in memory! - filterRegBean.setFilter(new ExcludePathAwareShallowETagFilter( - "/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", "/{tenant}/controller/artifacts/**", - "/{targetid}/softwaremodules/{softwareModuleId}/artifacts/**", "/api/v1/downloadserver/**")); + filterRegBean.setFilter( + new ExcludePathAwareShallowETagFilter("/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", + "/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/**", + "/api/v1/downloadserver/**")); return filterRegBean; } diff --git a/hawkbit-ddi-dl-api/README.md b/hawkbit-ddi-dl-api/README.md deleted file mode 100644 index ae8c9542e..000000000 --- a/hawkbit-ddi-dl-api/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Eclipse.IoT hawkBit - DDI Artifact Download API - Resources - -This module is part of the Direct Device Integration (DDI) API and is used by devices/targets for downloading artifacts through HTTP. - -# Version 1 - -The model follows [semantic versioning](http://semver.org) with MAJOR version, i.e. breaking changes will result in a new MAJOR version. - -# Compile - -#### Build hawkbit-ddi-dl-api - -``` -$ cd hawkbit/hawkbit-ddi-dl-api -$ mvn clean install -``` diff --git a/hawkbit-ddi-dl-api/pom.xml b/hawkbit-ddi-dl-api/pom.xml deleted file mode 100644 index 704267927..000000000 --- a/hawkbit-ddi-dl-api/pom.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - 4.0.0 - - org.eclipse.hawkbit - hawkbit-parent - 0.2.0-SNAPSHOT - - hawkbit-ddi-dl-api - hawkBit :: DDI Download Server (DL) API - - - - org.springframework.security - spring-security-web - - - diff --git a/hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlArtifactStoreControllerRestApi.java b/hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlArtifactStoreControllerRestApi.java deleted file mode 100644 index 19b9962a3..000000000 --- a/hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlArtifactStoreControllerRestApi.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2015 Bosch Software Innovations GmbH and others. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.eclipse.hawkbit.ddi.dl.rest.api; - -import java.io.InputStream; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * REST resource handling for artifact download operations. - */ -@RequestMapping(DdiDlRestConstants.ARTIFACTS_V1_REQUEST_MAPPING) -public interface DdiDlArtifactStoreControllerRestApi { - - /** - * Handles GET download request. This could be full or partial download - * request. - * - * @param tenant - * name of the client - * @param fileName - * to search for - * @param principal - * the authentication principal stored in the security context - * - * @return response of the servlet which in case of success is status code - * {@link HttpStatus#OK} or in case of partial download - * {@link HttpStatus#PARTIAL_CONTENT}. - */ - @RequestMapping(method = RequestMethod.GET, value = DdiDlRestConstants.ARTIFACT_DOWNLOAD_BY_FILENAME - + "/{fileName}") - @ResponseBody - ResponseEntity downloadArtifactByFilename(@PathVariable("tenant") final String tenant, - @PathVariable("fileName") final String fileName); - - /** - * Handles GET MD5 checksum file download request. - * - * @param tenant - * name of the client - * @param fileName - * to search for - * - * @return response of the servlet - */ - @RequestMapping(method = RequestMethod.GET, value = DdiDlRestConstants.ARTIFACT_DOWNLOAD_BY_FILENAME + "/{fileName}" - + DdiDlRestConstants.ARTIFACT_MD5_DWNL_SUFFIX) - @ResponseBody - ResponseEntity downloadArtifactMD5ByFilename(@PathVariable("tenant") final String tenant, - @PathVariable("fileName") final String fileName); - -} diff --git a/hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlRestConstants.java b/hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlRestConstants.java deleted file mode 100644 index 40ba5050f..000000000 --- a/hawkbit-ddi-dl-api/src/main/java/org/eclipse/hawkbit/ddi/dl/rest/api/DdiDlRestConstants.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2015 Bosch Software Innovations GmbH and others. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.eclipse.hawkbit.ddi.dl.rest.api; - -/** - * Constants for the direct device integration rest resources. - */ -public final class DdiDlRestConstants { - - /** - * The base URL mapping of the artifact repository rest resources. - */ - public static final String ARTIFACTS_V1_REQUEST_MAPPING = "/{tenant}/controller/artifacts/v1"; - - /** - * The artifact URL mapping rest resource. - */ - public static final String ARTIFACT_DOWNLOAD = "artifact"; - - /** - * The artifact by filename URL mapping rest resource. - */ - public static final String ARTIFACT_DOWNLOAD_BY_FILENAME = "/filename"; - - /** - * File suffix for MDH hash download (see Linux md5sum). - */ - public static final String ARTIFACT_MD5_DWNL_SUFFIX = ".MD5SUM"; - - // constant class, private constructor. - private DdiDlRestConstants() { - - } -} diff --git a/hawkbit-ddi-resource/pom.xml b/hawkbit-ddi-resource/pom.xml index 117a61346..a593e4551 100644 --- a/hawkbit-ddi-resource/pom.xml +++ b/hawkbit-ddi-resource/pom.xml @@ -26,11 +26,6 @@ hawkbit-ddi-api ${project.version} - - org.eclipse.hawkbit - hawkbit-ddi-dl-api - ${project.version} - org.eclipse.hawkbit hawkbit-rest-core 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 2e8a5f3fa..4b5f664c3 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 @@ -18,7 +18,6 @@ import org.eclipse.hawkbit.api.ApiType; import org.eclipse.hawkbit.api.ArtifactUrlHandler; import org.eclipse.hawkbit.api.URLPlaceholder; import org.eclipse.hawkbit.api.URLPlaceholder.SoftwareData; -import org.eclipse.hawkbit.ddi.dl.rest.api.DdiDlRestConstants; import org.eclipse.hawkbit.ddi.json.model.DdiArtifact; import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash; import org.eclipse.hawkbit.ddi.json.model.DdiChunk; @@ -160,7 +159,7 @@ public final class DataConversionHelper { final StringBuilder header = new StringBuilder(); header.append("attachment;filename="); header.append(fileName); - header.append(DdiDlRestConstants.ARTIFACT_MD5_DWNL_SUFFIX); + header.append(DdiRestConstants.ARTIFACT_MD5_DWNL_SUFFIX); response.setContentLength(content.length); response.setHeader("Content-Disposition", header.toString()); diff --git a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java deleted file mode 100644 index 21cdc39aa..000000000 --- a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright (c) 2015 Bosch Software Innovations GmbH and others. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.eclipse.hawkbit.ddi.rest.resource; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Optional; - -import javax.servlet.http.HttpServletRequest; - -import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; -import org.eclipse.hawkbit.ddi.dl.rest.api.DdiDlArtifactStoreControllerRestApi; -import org.eclipse.hawkbit.im.authentication.UserPrincipal; -import org.eclipse.hawkbit.repository.ArtifactManagement; -import org.eclipse.hawkbit.repository.ControllerManagement; -import org.eclipse.hawkbit.repository.EntityFactory; -import org.eclipse.hawkbit.repository.RepositoryConstants; -import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException; -import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; -import org.eclipse.hawkbit.repository.exception.SoftwareModuleNotAssignedToTargetException; -import org.eclipse.hawkbit.repository.model.Action; -import org.eclipse.hawkbit.repository.model.Action.Status; -import org.eclipse.hawkbit.repository.model.ActionStatus; -import org.eclipse.hawkbit.repository.model.Artifact; -import org.eclipse.hawkbit.repository.model.Target; -import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder; -import org.eclipse.hawkbit.rest.util.RestResourceConversionHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -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.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; - -/** - * The {@link DdiArtifactStoreController} of the HawkBit server controller API - * that is queried by the HawkBit target in order to download artifacts - * independent of their own individual resource. This is offered in addition to - * the {@link DdiRootController#downloadArtifact(String, Long, String)} for - * legacy controllers that can not be fed with a download URI at runtime. - */ -@RestController -@Scope(WebApplicationContext.SCOPE_REQUEST) -public class DdiArtifactStoreController implements DdiDlArtifactStoreControllerRestApi { - - private static final Logger LOG = LoggerFactory.getLogger(DdiArtifactStoreController.class); - - @Autowired - private ArtifactManagement artifactManagement; - - @Autowired - private ControllerManagement controllerManagement; - - @Autowired - private RequestResponseContextHolder requestResponseContextHolder; - - @Autowired - private EntityFactory entityFactory; - - @Override - public ResponseEntity downloadArtifactByFilename(@PathVariable("tenant") final String tenant, - @PathVariable("fileName") final String fileName) { - final Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - final Optional foundArtifacts = artifactManagement.findArtifactByFilename(fileName); - - if (!foundArtifacts.isPresent()) { - LOG.warn("Software artifact with name {} could not be found.", fileName); - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - ResponseEntity result; - final Artifact artifact = foundArtifacts.get(); - - final String ifMatch = requestResponseContextHolder.getHttpServletRequest().getHeader("If-Match"); - if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) { - result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED); - } else { - final DbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash()) - .orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash())); - - // we set a download status only if we are aware of the - // targetid, i.e. authenticated and not anonymous - if (principal instanceof UserPrincipal && ((UserPrincipal) principal).getUsername() != null - && !"anonymous".equals(((UserPrincipal) principal).getUsername())) { - final ActionStatus actionStatus = checkAndReportDownloadByTarget( - requestResponseContextHolder.getHttpServletRequest(), ((UserPrincipal) principal).getUsername(), - artifact); - result = RestResourceConversionHelper.writeFileResponse(artifact, - requestResponseContextHolder.getHttpServletResponse(), - requestResponseContextHolder.getHttpServletRequest(), file, controllerManagement, - actionStatus.getId()); - } else { - result = RestResourceConversionHelper.writeFileResponse(artifact, - requestResponseContextHolder.getHttpServletResponse(), - requestResponseContextHolder.getHttpServletRequest(), file); - } - - } - return result; - } - - @Override - public ResponseEntity downloadArtifactMD5ByFilename(@PathVariable("tenant") final String tenant, - @PathVariable("fileName") final String fileName) { - final Optional foundArtifacts = artifactManagement.findArtifactByFilename(fileName); - - if (!foundArtifacts.isPresent()) { - LOG.warn("Software artifact with name {} could not be found.", fileName); - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - try { - DataConversionHelper.writeMD5FileResponse(fileName, requestResponseContextHolder.getHttpServletResponse(), - foundArtifacts.get()); - } catch (final IOException e) { - LOG.error("Failed to stream MD5 File", e); - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } - - return new ResponseEntity<>(HttpStatus.OK); - } - - private ActionStatus checkAndReportDownloadByTarget(final HttpServletRequest request, final String controllerId, - final Artifact artifact) { - final Target target = controllerManagement.findByControllerId(controllerId) - .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); - - final Action action = controllerManagement - .getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), - artifact.getSoftwareModule().getId()) - .orElseThrow(() -> new SoftwareModuleNotAssignedToTargetException(artifact.getSoftwareModule().getId(), - target.getControllerId())); - final String range = request.getHeader("Range"); - - String message; - if (range != null) { - message = RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads range " + range + " of: " - + request.getRequestURI(); - } else { - message = RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads: " + request.getRequestURI(); - } - - return controllerManagement.addInformationalActionStatus( - entityFactory.actionStatus().create(action.getId()).status(Status.DOWNLOAD).message(message)); - } -} diff --git a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java index 95fe29bdc..70583ae1d 100644 --- a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java +++ b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java @@ -31,8 +31,6 @@ import java.util.TimeZone; import org.apache.commons.lang3.RandomUtils; import org.eclipse.hawkbit.ddi.rest.resource.DdiArtifactDownloadTest.DownloadTestConfiguration; import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent; -import org.eclipse.hawkbit.repository.model.Action; -import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; @@ -42,9 +40,8 @@ import org.junit.Before; import org.junit.Test; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort.Direction; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; @@ -66,8 +63,8 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { private static final int ARTIFACT_SIZE = 5 * 1024 * 1024; - private volatile static int downLoadProgress = 0; - private volatile static long shippedBytes = 0; + private static volatile int downLoadProgress = 0; + private static volatile long shippedBytes = 0; private final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); @@ -93,149 +90,71 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { ds.findFirstModuleByType(osType).get().getId(), "file1", false); // no artifact available - mvc.perform(get("/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/123455", + mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455", target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound()); - mvc.perform(get("/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/123455.MD5SUM", + mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455.MD5SUM", target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound()); // SM does not exist - mvc.perform(get("/controller/v1/{targetid}/softwaremodules/1234567890/artifacts/{filename}", + mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}", target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound()); - mvc.perform(get("/controller/v1/{targetid}/softwaremodules/1234567890/artifacts/{filename}.MD5SUM", + mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}.MD5SUM", target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound()); // test now consistent data to test allowed methods - mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()) .header(HttpHeaders.IF_MATCH, artifact.getSha1Hash())) .andExpect(status().isOk()); - mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isOk()); // test failed If-match - mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()) .header("If-Match", "fsjkhgjfdhg")) .andExpect(status().isPreconditionFailed()); // test invalid range - mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()) .header("Range", "bytes=1-10,hdsfjksdh")) .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) .andExpect(status().isRequestedRangeNotSatisfiable()); - mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()) .header("Range", "bytes=100-10")) .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) .andExpect(status().isRequestedRangeNotSatisfiable()); // not allowed methods - mvc.perform(put("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); - mvc.perform(delete("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); - mvc.perform(post("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); - mvc.perform(put("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); - mvc.perform(delete("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); - mvc.perform(post("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); } - @Test - @WithUser(principal = TestdataFactory.DEFAULT_CONTROLLER_ID, authorities = "ROLE_CONTROLLER", allSpPermissions = true) - @Description("Tests non allowed requests on the artifact ressource, e.g. invalid URI, wrong if-match, wrong command.") - public void invalidRequestsOnArtifactResourceByName() throws Exception { - // create target - final Target target = testdataFactory.createTarget(); - final List targets = Lists.newArrayList(target); - - // create ds - final DistributionSet ds = testdataFactory.createDistributionSet(""); - assignDistributionSet(ds, targets); - - // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024); - - // Binary - // no artifact available - mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1")).andExpect(status().isNotFound()); - - // no artifact available - mvc.perform(get("/controller/artifacts/v1/filename/{filename}.MD5SUM", "file1")) - .andExpect(status().isNotFound()); - - // test now consistent data to test allowed methods - final Artifact artifact = artifactManagement.createArtifact(new ByteArrayInputStream(random), getOsModule(ds), - "file1", false); - - mvc.perform( - get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") - .header(HttpHeaders.IF_MATCH, artifact.getSha1Hash())) - .andExpect(status().isOk()); - - mvc.perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", tenantAware.getCurrentTenant(), - "file1")).andExpect(status().isOk()); - - // test failed If-match - mvc.perform( - get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") - .header("If-Match", "fsjkhgjfdhg")) - .andExpect(status().isPreconditionFailed()); - - // test invalid range - mvc.perform( - get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") - .header("Range", "bytes=1-10,hdsfjksdh")) - .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) - .andExpect(status().isRequestedRangeNotSatisfiable()); - - mvc.perform( - get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") - .header("Range", "bytes=100-10")) - .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) - .andExpect(status().isRequestedRangeNotSatisfiable()); - - // not allowed methods - mvc.perform( - put("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1")) - .andExpect(status().isMethodNotAllowed()); - - mvc.perform(delete("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), - "file1")).andExpect(status().isMethodNotAllowed()); - - mvc.perform( - post("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1")) - .andExpect(status().isMethodNotAllowed()); - - // not allowed methods - mvc.perform(put("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", tenantAware.getCurrentTenant(), - "file1")).andExpect(status().isMethodNotAllowed()); - - mvc.perform(delete("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", - tenantAware.getCurrentTenant(), "file1")).andExpect(status().isMethodNotAllowed()); - - mvc.perform(post("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", tenantAware.getCurrentTenant(), - "file1")).andExpect(status().isMethodNotAllowed()); - - } - @Test @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) @Description("Tests valid downloads through the artifact resource by identifying the artifact not by ID but file name.") @@ -257,14 +176,14 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { ds.findFirstModuleByType(osType).get().getId(), "file1", false); // download fails as artifact is not yet assigned - mvc.perform(get("/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", target.getControllerId(), getOsModule(ds), artifact.getFilename())).andExpect(status().isNotFound()); // now assign and download successful assignDistributionSet(ds, targets); final MvcResult result = mvc .perform( - get("/{tenant}/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) @@ -298,7 +217,7 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { // download final MvcResult result = mvc .perform( - get("/{tenant}/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM", + get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isOk()).andExpect(header().string("Content-Disposition", @@ -309,95 +228,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { new String(artifact.getMd5Hash() + " " + artifact.getFilename()).getBytes(Charsets.US_ASCII)); } - @Test - @WithUser(authorities = "ROLE_CONTROLLER_ANONYMOUS", allSpPermissions = true) - @Description("Ensures that even an authenticated controller is not permitted to download if " - + "anonymous as authorization is notpossible, e.g. chekc if the controller has the artifact assigned.") - public void downloadArtifactByNameFailsIfNotAuthenticated() throws Exception { - downLoadProgress = 1; - shippedBytes = 0; - - assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); - - // create target - final Target target = testdataFactory.createTarget(); - final List targets = Lists.newArrayList(target); - - // create ds - final DistributionSet ds = testdataFactory.createDistributionSet(""); - - // create artifact - final byte random[] = RandomUtils.nextBytes(ARTIFACT_SIZE); - artifactManagement.createArtifact(new ByteArrayInputStream(random), getOsModule(ds), "file1.tar.bz2", false); - - // download fails as artifact is not yet assigned to target - assignDistributionSet(ds, targets); - mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1.tar.bz2")) - .andExpect(status().isNotFound()); - - assertThat(downLoadProgress).isEqualTo(1); - assertThat(shippedBytes).isEqualTo(0L); - } - - @Test - @WithUser(principal = TestdataFactory.DEFAULT_CONTROLLER_ID, authorities = "ROLE_CONTROLLER", allSpPermissions = true) - @Description("Ensures that an authenticated and named controller is permitted to download.") - public void downloadArtifactByNameByNamedController() throws Exception { - downLoadProgress = 1; - shippedBytes = 0; - - assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); - - // create target - final Target target = testdataFactory.createTarget(); - final List targets = Lists.newArrayList(target); - - // create ds - final DistributionSet ds = testdataFactory.createDistributionSet(""); - - // create artifact - final byte random[] = RandomUtils.nextBytes(ARTIFACT_SIZE); - final Artifact artifact = artifactManagement.createArtifact(new ByteArrayInputStream(random), getOsModule(ds), - "file1", false); - - // download fails as artifact is not yet assigned to target - mvc.perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), - "file1.tar.bz2")).andExpect(status().isNotFound()); - - // now assign and download successful - assignDistributionSet(ds, targets); - final MvcResult result = mvc - .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), - "file1")) - .andExpect(status().isOk()).andExpect(header().string("ETag", artifact.getSha1Hash())) - .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) - .andExpect(header().string("Accept-Ranges", "bytes")) - .andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt())))) - .andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn(); - - assertTrue("The same file that was uploaded is expected when downloaded", - Arrays.equals(result.getResponse().getContentAsByteArray(), random)); - - // one (update) action - assertThat(deploymentManagement.countActionsByTarget(target.getControllerId())).isEqualTo(1); - final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), pageReq).getContent() - .get(0); - - // one status - download - assertThat(action.getActionStatus()).hasSize(2); - assertThat(deploymentManagement - .findActionStatusByAction(new PageRequest(0, 400, Direction.DESC, "id"), action.getId()).getContent() - .get(0).getStatus()).isEqualTo(Status.DOWNLOAD); - - // download complete - assertThat(downLoadProgress).isEqualTo(10); - assertThat(shippedBytes).isEqualTo(ARTIFACT_SIZE); - } - @Test @WithUser(principal = TestdataFactory.DEFAULT_CONTROLLER_ID, authorities = "ROLE_CONTROLLER", allSpPermissions = true) @Description("Test various HTTP range requests for artifact download, e.g. chunk download or download resume.") - public void rangeDownloadArtifactByName() throws Exception { + public void rangeDownloadArtifact() throws Exception { // create target final Target target = testdataFactory.createTarget(); final List targets = Lists.newArrayList(target); @@ -425,8 +259,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { final String rangeString = "" + i * range + "-" + ((i + 1) * range - 1); final MvcResult result = mvc - .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", - tenantAware.getCurrentTenant(), "file1").header("Range", "bytes=" + rangeString)) + .perform( + get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1") + .header("Range", "bytes=" + rangeString)) .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) .andExpect(header().string("Accept-Ranges", "bytes")) @@ -442,8 +278,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { // return last 1000 Bytes MvcResult result = mvc - .perform(get("/${tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), - "file1").header("Range", "bytes=-1000")) + .perform( + get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1") + .header("Range", "bytes=-1000")) .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) .andExpect(header().string("Accept-Ranges", "bytes")) @@ -458,8 +296,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { // skip first 1000 Bytes and return the rest result = mvc - .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), - "file1").header("Range", "bytes=1000-")) + .perform( + get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1") + .header("Range", "bytes=1000-")) .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) .andExpect(header().string("Accept-Ranges", "bytes")) @@ -474,8 +314,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { // multipart download - first 20 bytes in 2 parts result = mvc - .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), - "file1").header("Range", "bytes=0-9,10-19")) + .perform( + get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1") + .header("Range", "bytes=0-9,10-19")) .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) .andExpect(content().contentType("multipart/byteranges; boundary=THIS_STRING_SEPARATES_MULTIPART")) .andExpect(header().string("Accept-Ranges", "bytes")) @@ -497,54 +339,7 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { } - @Test - @Description("Ensures that the download fails if te controller is not authenticated.") - public void faildDownloadArtifactByNameIfAuthenticationMissing() throws Exception { - assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); - - // create target - final Target target = testdataFactory.createTarget(); - final List targets = Lists.newArrayList(target); - - // create ds - final DistributionSet ds = testdataFactory.createDistributionSet(""); - - // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024); - final Artifact artifact = artifactManagement.createArtifact(new ByteArrayInputStream(random), getOsModule(ds), - "file1.tar.bz2", false); - - // download fails as artifact is not yet assigned to target - mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1.tar.bz2")) - .andExpect(status().isNotFound()); - } - - @Test - @Description("Downloads an MD5SUM file by the related artifacts filename.") - public void downloadMd5sumFileByName() throws Exception { - // create target - final Target target = testdataFactory.createTarget(); - - // create ds - final DistributionSet ds = testdataFactory.createDistributionSet(""); - - // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024); - final Artifact artifact = artifactManagement.createArtifact(new ByteArrayInputStream(random), getOsModule(ds), - "file1.tar.bz2", false); - - // download - final MvcResult result = mvc - .perform(get("/{tenant}/controller/artifacts/v1/filename/file1.tar.bz2.MD5SUM", - tenantAware.getCurrentTenant())) - .andExpect(status().isOk()) - .andExpect(header().string("Content-Disposition", "attachment;filename=file1.tar.bz2.MD5SUM")) - .andReturn(); - - assertThat(result.getResponse().getContentAsByteArray()) - .isEqualTo(new String(artifact.getMd5Hash() + " file1.tar.bz2").getBytes(Charsets.US_ASCII)); - } - + @Configuration public static class DownloadTestConfiguration { @Bean @@ -557,7 +352,7 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { private static class Listener { @EventListener(classes = DownloadProgressEvent.class) - public void listen(final DownloadProgressEvent event) { + public static void listen(final DownloadProgressEvent event) { downLoadProgress++; shippedBytes += event.getShippedBytesSinceLast(); diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index e4af4c8bf..4ef020f1e 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -304,7 +304,9 @@ public abstract class AbstractIntegrationTest implements EnvironmentAware { .addFilter(new DosFilter(100, 10, "127\\.0\\.0\\.1|\\[0:0:0:0:0:0:0:1\\]", "(^192\\.168\\.)", "X-Forwarded-For")) .addFilter(new ExcludePathAwareShallowETagFilter( - "/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", "/*/controller/artifacts/**")); + "/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", + "/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/**", + "/api/v1/downloadserver/**")); } private static CIMySqlTestDatabase tesdatabase; diff --git a/hawkbit-runtime/hawkbit-update-server/src/main/java/org/eclipse/hawkbit/app/StreamAwareErrorController.java b/hawkbit-runtime/hawkbit-update-server/src/main/java/org/eclipse/hawkbit/app/StreamAwareErrorController.java new file mode 100644 index 000000000..e6bc8590d --- /dev/null +++ b/hawkbit-runtime/hawkbit-update-server/src/main/java/org/eclipse/hawkbit/app/StreamAwareErrorController.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.app; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.autoconfigure.web.BasicErrorController; +import org.springframework.boot.autoconfigure.web.ErrorAttributes; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Error page controller that ensures that ocet stream does not return text in + * case of an error. + * + */ +@Controller +public class StreamAwareErrorController extends BasicErrorController { + + /** + * A new {@link StreamAwareErrorController}. + * + * @param errorAttributes + * the error attributes + * @param serverProperties + * configuration properties + */ + public StreamAwareErrorController(final ErrorAttributes errorAttributes, final ServerProperties serverProperties) { + super(errorAttributes, serverProperties.getError()); + } + + @RequestMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + ResponseEntity errorStream(final HttpServletRequest request, final HttpServletResponse response) { + final HttpStatus status = getStatus(request); + return new ResponseEntity<>(status); + } + +} diff --git a/pom.xml b/pom.xml index 451ba1ee4..e484c2c3d 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,6 @@ hawkbit-mgmt-api hawkbit-mgmt-resource hawkbit-ddi-api - hawkbit-ddi-dl-api hawkbit-ddi-resource hawkbit-dmf hawkbit-repository