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 <kai.zimmermann@bosch-si.com>

* Add stream aware error controller.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fixed bean definition.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
Kai Zimmermann
2017-05-03 09:21:14 +02:00
committed by GitHub
parent 525669724f
commit 1ee3d0c850
12 changed files with 97 additions and 560 deletions

View File

@@ -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;
}

View File

@@ -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
```

View File

@@ -1,28 +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
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-parent</artifactId>
<version>0.2.0-SNAPSHOT</version>
</parent>
<artifactId>hawkbit-ddi-dl-api</artifactId>
<name>hawkBit :: DDI Download Server (DL) API</name>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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<InputStream> 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<Void> downloadArtifactMD5ByFilename(@PathVariable("tenant") final String tenant,
@PathVariable("fileName") final String fileName);
}

View File

@@ -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() {
}
}

View File

@@ -26,11 +26,6 @@
<artifactId>hawkbit-ddi-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-ddi-dl-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-rest-core</artifactId>

View File

@@ -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());

View File

@@ -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<InputStream> downloadArtifactByFilename(@PathVariable("tenant") final String tenant,
@PathVariable("fileName") final String fileName) {
final Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
final Optional<Artifact> 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<InputStream> 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<Void> downloadArtifactMD5ByFilename(@PathVariable("tenant") final String tenant,
@PathVariable("fileName") final String fileName) {
final Optional<Artifact> 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));
}
}

View File

@@ -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<Target> 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<Target> 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<Target> 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<Target> 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<Target> 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();

View File

@@ -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;

View File

@@ -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<Void> errorStream(final HttpServletRequest request, final HttpServletResponse response) {
final HttpStatus status = getStatus(request);
return new ResponseEntity<>(status);
}
}

View File

@@ -37,7 +37,6 @@
<module>hawkbit-mgmt-api</module>
<module>hawkbit-mgmt-resource</module>
<module>hawkbit-ddi-api</module>
<module>hawkbit-ddi-dl-api</module>
<module>hawkbit-ddi-resource</module>
<module>hawkbit-dmf</module>
<module>hawkbit-repository</module>