From 100137ca9740185e509d984b429b3ab1827ddc25 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Tue, 7 Dec 2021 16:26:58 +0100 Subject: [PATCH] Improved validation of list of links upload --- .../ui/controller/CreateLinksController.java | 40 +++++++++++++++++-- .../src/main/resources/application.properties | 1 + .../controller/CreateLinksControllerTest.java | 36 +++++++++++++++++ .../src/test/resources/list-of-links.txt | 6 +-- .../src/components/modal/CreateLinksModal.vue | 5 ++- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java index 8d0ea52..b3dfe7f 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java @@ -10,6 +10,8 @@ import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -18,6 +20,7 @@ import net.ivoa.xml.vospace.v2.LinkNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -31,6 +34,9 @@ public class CreateLinksController extends BaseController { private static final Logger LOG = LoggerFactory.getLogger(CreateLinksController.class); + @Value("${list-of-links.limit:1000}") + private int listOfLinksSizeLimit; + @Autowired private VOSpaceClient client; @@ -63,11 +69,17 @@ public class CreateLinksController extends BaseController { List<CompletableFuture<?>> currentHttpCallsGroup = new ArrayList<>(); httpCallsGroups.add(currentHttpCallsGroup); - for (String url : fileContent.replaceAll("\\r\\n?", "\n").split("\n")) { // normalize newlines and split on them + // normalize newlines and split on them + String[] urls = fileContent.replaceAll("\\r\\n?", "\n").split("\n"); + if (urls.length > listOfLinksSizeLimit) { + throw new BadRequestException("List is too large: " + urls.length + " lines detected, limit is " + listOfLinksSizeLimit); + } + + for (String url : urls) { if (!url.isBlank()) { - String fileName = url.substring(url.lastIndexOf("/") + 1); - String uri = parent.getUri() + "/" + fileName; + url = url.trim(); + String uri = parent.getUri() + "/" + getFileNameFromUrl(url); LinkNode link = new LinkNode(); link.setUri(uri); @@ -89,6 +101,28 @@ public class CreateLinksController extends BaseController { return ResponseEntity.noContent().build(); } + private String getFileNameFromUrl(String url) { + + try { + // parse URL and remove the query string + String urlPath = new URL(url).getPath(); + if (urlPath.endsWith("/")) { + // remove last char if it is a slash + urlPath = urlPath.substring(0, urlPath.length() - 1); + } + if (urlPath.isEmpty() || !urlPath.contains("/")) { + throw new BadRequestException("Unable to extract file name from URL " + url); + } + String fileName = urlPath.substring(urlPath.lastIndexOf("/") + 1); + if (fileName.isEmpty()) { + throw new BadRequestException("Unable to extract file name from URL " + url); + } + return fileName; + } catch (MalformedURLException ex) { + throw new BadRequestException("Invalid URL: " + url); + } + } + private ContainerNode getFolder(String folderPath) { try { return (ContainerNode) client.getNode("/" + folderPath); diff --git a/vospace-ui-backend/src/main/resources/application.properties b/vospace-ui-backend/src/main/resources/application.properties index e72cdd4..dc3177c 100644 --- a/vospace-ui-backend/src/main/resources/application.properties +++ b/vospace-ui-backend/src/main/resources/application.properties @@ -11,6 +11,7 @@ cors.allowed.origin=http://localhost:8080 logging.level.it.inaf=TRACE trusted.eppn.scope=inaf.it +list-of-links.limit=1000 support.contact.label=IA2 team support.contact.email=ia2@inaf.it diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java index f1eb4d7..92c56f3 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java @@ -10,6 +10,7 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; +import java.util.Collections; import net.ivoa.xml.vospace.v2.ContainerNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -109,6 +110,41 @@ public class CreateLinksControllerTest { verify(client, times(75)).createNode(any()); } + @Test + public void testInvalidUrl() throws Exception { + testInvalidContent("foo"); + } + + @Test + public void testInvalidUrlNoFile() throws Exception { + testInvalidContent("http://archives.ia2.inaf.it/"); + } + + @Test + public void testInvalidUrlNoFileNoSlash() throws Exception { + testInvalidContent("http://archives.ia2.inaf.it"); + } + + @Test + public void testTooManyLinks() throws Exception { + testInvalidContent(String.join("\n", Collections.nCopies(1500, "http://archives.ia2.inaf.it/files/aao/SC182172.fits.gz"))); + } + + private void testInvalidContent(String fileContent) throws Exception { + + ContainerNode myFolder = new ContainerNode(); + when(client.getNode("/path/to/myfolder")).thenReturn(myFolder); + + MockMultipartFile file = new MockMultipartFile("file", fileContent.getBytes()); + + mockMvc.perform(multipart("/uploadLinks") + .file(file) + .param("folder", "path/to/myfolder") + .sessionAttr("user_data", user)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + private MockMultipartFile getListOfLinksMockMultipartFile() throws Exception { return new MockMultipartFile("file", UploadControllerTest.class.getClassLoader().getResourceAsStream("list-of-links.txt")); } diff --git a/vospace-ui-backend/src/test/resources/list-of-links.txt b/vospace-ui-backend/src/test/resources/list-of-links.txt index 0474fce..b3d48ff 100644 --- a/vospace-ui-backend/src/test/resources/list-of-links.txt +++ b/vospace-ui-backend/src/test/resources/list-of-links.txt @@ -1,7 +1,7 @@ http://archives.ia2.inaf.it/files/aao/SC182159.fits.gz -http://archives.ia2.inaf.it/files/aao/SC182160.fits.gz -http://archives.ia2.inaf.it/files/aao/SC182161.fits.gz -http://archives.ia2.inaf.it/files/aao/SC182169.fits.gz +http://archives.ia2.inaf.it/files/aao/SC182160.fits.gz/ +http://archives.ia2.inaf.it/files/aao/SC182161.fits.gz?query=xxx +http://archives.ia2.inaf.it/files/aao/SC182169.fits.gz http://archives.ia2.inaf.it/files/aao/SC182170.fits.gz http://archives.ia2.inaf.it/files/aao/SC182171.fits.gz http://archives.ia2.inaf.it/files/aao/SC182172.fits.gz diff --git a/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue b/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue index ec1f540..eee6cbc 100644 --- a/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue +++ b/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue @@ -24,7 +24,10 @@ <b-form-invalid-feedback id="node-name-input-feedback" class="text-right">{{nodeNameError}}</b-form-invalid-feedback> </b-form> <b-form inline v-if="mode === 'multiple'"> - <p>Upload list of links (separated by newlines)</p> + <p> + Upload list of links (separated by newlines)<br /> + <em>Maximum 1000 links per file!</em> + </p> <b-form-file class="text-left" v-model="file" :multiple="false" placeholder="Choose your file or drop it here..." drop-placeholder="Drop file here..." :state="fileState"></b-form-file> <b-form-invalid-feedback id="file-input-feedback" class="text-right">{{fileError}}</b-form-invalid-feedback> </b-form> -- GitLab