Skip to content

Commit f4e6fb0

Browse files
committed
Backport REST files fixes to 1.6 #3836
1 parent d44f8b1 commit f4e6fb0

4 files changed

Lines changed: 86 additions & 10 deletions

File tree

jmix-localfs/localfs/src/main/java/io/jmix/localfs/LocalFileStorage.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
package io.jmix.localfs;
1818

1919
import com.google.common.util.concurrent.ThreadFactoryBuilder;
20-
import io.jmix.core.CoreProperties;
21-
import io.jmix.core.FileRef;
22-
import io.jmix.core.FileStorage;
23-
import io.jmix.core.FileStorageException;
24-
import io.jmix.core.TimeSource;
25-
import io.jmix.core.UuidProvider;
20+
import io.jmix.core.*;
2621
import io.jmix.core.annotation.Internal;
2722
import org.apache.commons.io.FileUtils;
2823
import org.apache.commons.io.FilenameUtils;
@@ -31,6 +26,7 @@
3126
import org.slf4j.Logger;
3227
import org.slf4j.LoggerFactory;
3328
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.beans.factory.annotation.Value;
3430
import org.springframework.stereotype.Component;
3531

3632
import javax.annotation.PreDestroy;
@@ -69,6 +65,9 @@ public class LocalFileStorage implements FileStorage {
6965
@Autowired
7066
protected TimeSource timeSource;
7167

68+
@Value("${jmix.localfs.disable-path-check:false}")
69+
protected Boolean disablePathCheck;
70+
7271
protected boolean isImmutableFileStorage;
7372

7473
protected ExecutorService writeExecutor = Executors.newFixedThreadPool(5,
@@ -160,10 +159,28 @@ public long saveStream(FileRef fileRef, InputStream inputStream) {
160159
checkFileExists(path);
161160

162161
long size;
162+
long maxAllowedSize = properties.getMaxFileSize().toBytes();
163163
try (OutputStream outputStream = Files.newOutputStream(path, CREATE_NEW)) {
164-
size = IOUtils.copyLarge(inputStream, outputStream);
164+
size = IOUtils.copyLarge(inputStream, outputStream, 0, maxAllowedSize);
165+
166+
if (size >= maxAllowedSize) {
167+
if (inputStream.read() != IOUtils.EOF) {
168+
outputStream.close();
169+
if (path.toFile().exists()) {
170+
if (!path.toFile().delete()) {
171+
log.warn("Failed to delete an incorrectly uploaded file '{}'. " +
172+
"File was to large and has been rejected but already loaded part was not deleted.",
173+
path.toAbsolutePath());
174+
}
175+
}
176+
throw new FileStorageException(FileStorageException.Type.IO_EXCEPTION,
177+
String.format("File is too large: '%s'. Max file size = %s MB is exceeded but there are unread bytes left.",
178+
path.toAbsolutePath(),
179+
properties.getMaxFileSize().toMegabytes()));
180+
}
181+
}
165182
outputStream.flush();
166-
// writeLog(path, false);
183+
//writeLog(path, false);
167184
} catch (IOException e) {
168185
FileUtils.deleteQuietly(path.toFile());
169186
throw new FileStorageException(FileStorageException.Type.IO_EXCEPTION, path.toAbsolutePath().toString(), e);
@@ -222,6 +239,11 @@ public InputStream openStream(FileRef reference) {
222239
}
223240

224241
try {
242+
if (!Boolean.TRUE.equals(disablePathCheck) && !path.toRealPath().startsWith(root.toRealPath())) {
243+
log.error("File '{}' is outside of root dir '{}': ", path, root);
244+
continue;
245+
}
246+
225247
inputStream = Files.newInputStream(path);
226248
} catch (IOException e) {
227249
log.error("Error opening input stream for " + path, e);

jmix-localfs/localfs/src/main/java/io/jmix/localfs/LocalFileStorageProperties.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import org.springframework.boot.context.properties.ConfigurationProperties;
2020
import org.springframework.boot.context.properties.ConstructorBinding;
21+
import org.springframework.boot.context.properties.bind.DefaultValue;
22+
import org.springframework.util.unit.DataSize;
2123

2224
@ConfigurationProperties(prefix = "jmix.localfs")
2325
@ConstructorBinding
@@ -28,9 +30,16 @@ public class LocalFileStorageProperties {
2830
*/
2931
String storageDir;
3032

33+
/**
34+
* Maximum allowable file size.
35+
*/
36+
DataSize maxFileSize;
37+
3138
public LocalFileStorageProperties(
32-
String storageDir) {
39+
String storageDir,
40+
@DefaultValue("100MB") DataSize maxFileSize) {
3341
this.storageDir = storageDir;
42+
this.maxFileSize = maxFileSize;
3443
}
3544

3645
/**
@@ -39,4 +48,11 @@ public LocalFileStorageProperties(
3948
public String getStorageDir() {
4049
return storageDir;
4150
}
51+
52+
/**
53+
* @see #maxFileSize
54+
*/
55+
public DataSize getMaxFileSize() {
56+
return maxFileSize;
57+
}
4258
}

jmix-rest/rest/src/main/java/io/jmix/rest/RestProperties.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import javax.annotation.Nullable;
2525
import java.util.Collections;
2626
import java.util.Map;
27+
import java.util.Set;
2728

2829
@ConfigurationProperties(prefix = "jmix.rest")
2930
@ConstructorBinding
@@ -47,18 +48,24 @@ public class RestProperties {
4748
private final boolean responseFetchPlanEnabled;
4849
private final int defaultMaxFetchSize;
4950
private final Map<String, Integer> entityMaxFetchSize;
51+
/**
52+
* File extensions that can be opened for viewing in a browser by replying with 'Content-Disposition=inline' header.
53+
*/
54+
protected Set<String> inlineEnabledFileExtensions;
5055

5156
public RestProperties(
5257
@DefaultValue("*") String[] allowedOrigins,
5358
@DefaultValue("false") boolean optimisticLockingEnabled,
5459
@DefaultValue("true") boolean responseFetchPlanEnabled,
5560
@DefaultValue("10000") int defaultMaxFetchSize,
61+
@DefaultValue({"jpg", "png", "jpeg", "pdf"}) Set<String> inlineEnabledFileExtensions,
5662
@Nullable Map<String, Integer> entityMaxFetchSize) {
5763
this.allowedOrigins = allowedOrigins;
5864
this.optimisticLockingEnabled = optimisticLockingEnabled;
5965
this.responseFetchPlanEnabled = responseFetchPlanEnabled;
6066
this.defaultMaxFetchSize = defaultMaxFetchSize;
6167
this.entityMaxFetchSize = entityMaxFetchSize == null ? Collections.emptyMap() : entityMaxFetchSize;
68+
this.inlineEnabledFileExtensions = inlineEnabledFileExtensions;
6269
}
6370

6471
/**
@@ -85,6 +92,13 @@ public String[] getAllowedOrigins() {
8592
return allowedOrigins;
8693
}
8794

95+
/**
96+
* @see #inlineEnabledFileExtensions
97+
*/
98+
public Set<String> getInlineEnabledFileExtensions() {
99+
return inlineEnabledFileExtensions;
100+
}
101+
88102
public int getEntityMaxFetchSize(String entityName) {
89103
return entityMaxFetchSize.getOrDefault(entityName, defaultMaxFetchSize);
90104
}

jmix-rest/rest/src/main/java/io/jmix/rest/impl/controller/FileDownloadController.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
package io.jmix.rest.impl.controller;
1818

1919
import io.jmix.core.AccessManager;
20-
import io.jmix.core.FileTransferService;
2120
import io.jmix.core.FileRef;
21+
import io.jmix.core.FileTransferService;
2222
import io.jmix.core.Metadata;
23+
import io.jmix.rest.RestProperties;
2324
import io.jmix.rest.accesscontext.RestFileDownloadContext;
2425
import io.jmix.rest.exception.RestAPIException;
26+
import org.apache.commons.io.FilenameUtils;
27+
import org.apache.commons.lang3.BooleanUtils;
28+
import org.apache.commons.lang3.StringUtils;
2529
import org.slf4j.Logger;
2630
import org.slf4j.LoggerFactory;
2731
import org.springframework.beans.factory.annotation.Autowired;
@@ -32,6 +36,7 @@
3236
import org.springframework.web.bind.annotation.RestController;
3337

3438
import javax.servlet.http.HttpServletResponse;
39+
import java.util.Set;
3540

3641
/**
3742
* REST API controller that is used for downloading files
@@ -48,6 +53,8 @@ public class FileDownloadController {
4853
protected AccessManager accessManager;
4954
@Autowired
5055
protected FileTransferService fileTransferService;
56+
@Autowired
57+
protected RestProperties restProperties;
5158

5259
@GetMapping
5360
public void downloadFile(@RequestParam String fileRef,
@@ -58,6 +65,7 @@ public void downloadFile(@RequestParam String fileRef,
5865
try {
5966
FileRef fileReference;
6067
fileReference = FileRef.fromString(fileRef);
68+
attachment = resolveAttachmentValue(attachment, fileReference);
6169
fileTransferService.downloadAndWriteResponse(fileReference, fileReference.getStorageName(), attachment, response);
6270
} catch (IllegalArgumentException e) {
6371
throw new RestAPIException("Invalid file reference",
@@ -75,4 +83,20 @@ protected void checkFileDownloadPermission() {
7583
throw new RestAPIException("File download failed", "File download is not permitted", HttpStatus.FORBIDDEN);
7684
}
7785
}
86+
87+
protected boolean resolveAttachmentValue(Boolean attachmentRequestParameterValue, FileRef fileRef) {
88+
if (BooleanUtils.isTrue(attachmentRequestParameterValue)) {
89+
return true;
90+
}
91+
String fileName = fileRef.getFileName();
92+
String extension = FilenameUtils.getExtension(fileName);
93+
if (StringUtils.isEmpty(extension)) {
94+
// No extension - just download
95+
return true;
96+
} else {
97+
// Check if file is allowed to be opened inline
98+
Set<String> inlineEnabledFileExtensions = restProperties.getInlineEnabledFileExtensions();
99+
return !inlineEnabledFileExtensions.contains(StringUtils.lowerCase(extension));
100+
}
101+
}
78102
}

0 commit comments

Comments
 (0)