mirror of https://github.com/halo-dev/halo
Secure backup file downloading
parent
8463a3f31e
commit
2153998a32
|
@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
import freemarker.template.TemplateException;
|
||||
import freemarker.template.TemplateExceptionHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.jackson.JsonComponentModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
@ -32,7 +31,9 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR;
|
||||
import static run.halo.app.model.support.HaloConst.HALO_ADMIN_RELATIVE_PATH;
|
||||
import static run.halo.app.utils.HaloUtils.*;
|
||||
|
||||
/**
|
||||
* Mvc configuration.
|
||||
|
@ -81,18 +82,20 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer {
|
|||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
String workDir = FILE_PROTOCOL + StringUtils.appendIfMissing(haloProperties.getWorkDir(), "/");
|
||||
String backupDir = FILE_PROTOCOL + StringUtils.appendIfMissing(haloProperties.getBackupDir(), "/");
|
||||
String workDir = FILE_PROTOCOL + ensureSuffix(haloProperties.getWorkDir(), FILE_SEPARATOR);
|
||||
String backupDir = FILE_PROTOCOL + ensureSuffix(haloProperties.getBackupDir(), FILE_SEPARATOR);
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations(workDir + "templates/themes/")
|
||||
.addResourceLocations(workDir + "templates/admin/")
|
||||
.addResourceLocations("classpath:/admin/")
|
||||
.addResourceLocations(workDir + "static/");
|
||||
registry.addResourceHandler(haloProperties.getUploadUrlPrefix() + "/**")
|
||||
|
||||
String uploadUrlPattern = ensureBoth(haloProperties.getUploadUrlPrefix(), URL_SEPARATOR) + "**";
|
||||
String adminPathPattern = ensureSuffix(haloProperties.getAdminPath(), URL_SEPARATOR) + "**";
|
||||
|
||||
registry.addResourceHandler(uploadUrlPattern)
|
||||
.addResourceLocations(workDir + "upload/");
|
||||
registry.addResourceHandler(haloProperties.getBackupUrlPrefix() + "/**")
|
||||
.addResourceLocations(workDir + "backup/", backupDir);
|
||||
registry.addResourceHandler(haloProperties.getAdminPath() + "/**")
|
||||
registry.addResourceHandler(adminPathPattern)
|
||||
.addResourceLocations(workDir + HALO_ADMIN_RELATIVE_PATH)
|
||||
.addResourceLocations("classpath:/admin/");
|
||||
|
||||
|
|
|
@ -2,14 +2,15 @@ package run.halo.app.config.properties;
|
|||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import run.halo.app.model.support.HaloConst;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
|
||||
import static run.halo.app.model.support.HaloConst.*;
|
||||
import static run.halo.app.utils.HaloUtils.ensureSuffix;
|
||||
|
||||
|
||||
/**
|
||||
* Halo configuration properties.
|
||||
|
@ -38,27 +39,22 @@ public class HaloProperties {
|
|||
/**
|
||||
* Admin path.
|
||||
*/
|
||||
private String adminPath = "/admin";
|
||||
private String adminPath = "admin";
|
||||
|
||||
/**
|
||||
* Halo backup directory.(Not recommended to modify this config);
|
||||
*/
|
||||
private String backupDir = HaloConst.TEMP_DIR + "/halo-backup/";
|
||||
private String backupDir = ensureSuffix(TEMP_DIR, FILE_SEPARATOR) + "halo-backup" + FILE_SEPARATOR;
|
||||
|
||||
/**
|
||||
* Work directory.
|
||||
*/
|
||||
private String workDir = HaloConst.USER_HOME + "/.halo/";
|
||||
private String workDir = ensureSuffix(USER_HOME, FILE_SEPARATOR) + ".halo" + FILE_SEPARATOR;
|
||||
|
||||
/**
|
||||
* Upload prefix.
|
||||
*/
|
||||
private String uploadUrlPrefix = "/upload";
|
||||
|
||||
/**
|
||||
* backup prefix.
|
||||
*/
|
||||
private String backupUrlPrefix = "/backup";
|
||||
private String uploadUrlPrefix = "upload";
|
||||
|
||||
/**
|
||||
* Download Timeout.
|
||||
|
|
|
@ -6,6 +6,10 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import run.halo.app.exception.FileOperationException;
|
||||
|
@ -13,6 +17,7 @@ import run.halo.app.model.dto.BackupDTO;
|
|||
import run.halo.app.model.dto.post.BasePostDetailDTO;
|
||||
import run.halo.app.service.BackupService;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
@ -49,6 +54,28 @@ public class BackupController {
|
|||
return backupService.listHaloBackups();
|
||||
}
|
||||
|
||||
@GetMapping("halo/{fileName:.+}")
|
||||
@ApiOperation("Download backup file")
|
||||
public ResponseEntity<Resource> downloadBackup(@PathVariable("fileName") String fileName, HttpServletRequest request) {
|
||||
log.info("Try to download backup file: [{}]", fileName);
|
||||
|
||||
// Load file as resource
|
||||
Resource backupResource = backupService.loadFileAsResource(fileName);
|
||||
|
||||
String contentType = "application/octet-stream";
|
||||
// Try to determine file's content type
|
||||
try {
|
||||
contentType = request.getServletContext().getMimeType(backupResource.getFile().getAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
log.warn("Could not determine file type", e);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + backupResource.getFilename() + "\"")
|
||||
.body(backupResource);
|
||||
}
|
||||
|
||||
@DeleteMapping("halo")
|
||||
@ApiOperation("Delete a backup")
|
||||
public void deleteBackup(@RequestParam("filename") String filename) {
|
||||
|
|
|
@ -7,7 +7,11 @@ import org.aspectj.lang.ProceedingJoinPoint;
|
|||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
@ -75,16 +79,36 @@ public class ControllerLogAop {
|
|||
|
||||
private void printResponseLog(HttpServletRequest request, String className, String methodName, Object returnObj, long usage) throws JsonProcessingException {
|
||||
if (log.isDebugEnabled()) {
|
||||
String returningData = null;
|
||||
String returnData = "";
|
||||
|
||||
if (returnObj != null) {
|
||||
if (returnObj.getClass().isAssignableFrom(byte[].class)) {
|
||||
returningData = "Binary data";
|
||||
if (returnObj instanceof ResponseEntity) {
|
||||
ResponseEntity responseEntity = (ResponseEntity) returnObj;
|
||||
if (responseEntity.getBody() instanceof Resource) {
|
||||
returnData = "[ BINARY DATA ]";
|
||||
} else {
|
||||
returnData = toString(responseEntity.getBody());
|
||||
}
|
||||
} else {
|
||||
returningData = JsonUtils.objectToJson(returnObj);
|
||||
returnData = toString(returnObj);
|
||||
}
|
||||
|
||||
}
|
||||
log.debug("{}.{} Response: [{}], usage: [{}]ms", className, methodName, returningData, usage);
|
||||
log.debug("{}.{} Response: [{}], usage: [{}]ms", className, methodName, returnData, usage);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String toString(@NonNull Object obj) throws JsonProcessingException {
|
||||
Assert.notNull(obj, "Return object must not be null");
|
||||
|
||||
String toString = "";
|
||||
if (obj.getClass().isAssignableFrom(byte[].class) && obj instanceof Resource) {
|
||||
toString = "[ BINARY DATA ]";
|
||||
} else {
|
||||
toString = JsonUtils.objectToJson(obj);
|
||||
}
|
||||
return toString;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.service;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import run.halo.app.model.dto.BackupDTO;
|
||||
|
@ -61,7 +62,16 @@ public interface BackupService {
|
|||
/**
|
||||
* Deletes backup.
|
||||
*
|
||||
* @param filename filename must not be blank
|
||||
* @param fileName filename must not be blank
|
||||
*/
|
||||
void deleteHaloBackup(@NonNull String filename);
|
||||
void deleteHaloBackup(@NonNull String fileName);
|
||||
|
||||
/**
|
||||
* Loads file as resource.
|
||||
*
|
||||
* @param fileName backup file name must not be blank.
|
||||
* @return resource of the given file
|
||||
*/
|
||||
@NonNull
|
||||
Resource loadFileAsResource(@NonNull String fileName);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import org.apache.commons.io.FileUtils;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -24,9 +26,11 @@ import run.halo.app.service.BackupService;
|
|||
import run.halo.app.service.OptionService;
|
||||
import run.halo.app.service.PostService;
|
||||
import run.halo.app.service.PostTagService;
|
||||
import run.halo.app.utils.HaloUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
|
@ -125,10 +129,8 @@ public class BackupServiceImpl implements BackupService {
|
|||
|
||||
if (StringUtils.isNotBlank(post.getPassword())) {
|
||||
passwords.add(one);
|
||||
continue;
|
||||
} else if (post.getDeleted()) {
|
||||
drafts.add(one);
|
||||
continue;
|
||||
} else {
|
||||
posts.add(one);
|
||||
}
|
||||
|
@ -159,10 +161,10 @@ public class BackupServiceImpl implements BackupService {
|
|||
// Zip work directory to temporary file
|
||||
try {
|
||||
// Create zip path for halo zip
|
||||
String haloZipFileName = new StringBuilder().append(HaloConst.HALO_BACKUP_PREFIX)
|
||||
.append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")))
|
||||
.append(IdUtil.simpleUUID())
|
||||
.append(".zip").toString();
|
||||
String haloZipFileName = HaloConst.HALO_BACKUP_PREFIX +
|
||||
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) +
|
||||
IdUtil.simpleUUID() +
|
||||
".zip";
|
||||
// Create halo zip file
|
||||
Path haloZipPath = Files.createFile(Paths.get(haloProperties.getBackupDir(), haloZipFileName));
|
||||
|
||||
|
@ -198,22 +200,42 @@ public class BackupServiceImpl implements BackupService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void deleteHaloBackup(String filename) {
|
||||
Assert.hasText(filename, "File name must not be blank");
|
||||
public void deleteHaloBackup(String fileName) {
|
||||
Assert.hasText(fileName, "File name must not be blank");
|
||||
|
||||
// Get backup path
|
||||
Path backupPath = Paths.get(haloProperties.getBackupDir(), filename);
|
||||
Path backupPath = Paths.get(haloProperties.getBackupDir(), fileName);
|
||||
|
||||
try {
|
||||
// Delete backup file
|
||||
Files.delete(backupPath);
|
||||
} catch (NoSuchFileException e) {
|
||||
throw new NotFoundException("The file " + filename + " was not found", e);
|
||||
throw new NotFoundException("The file " + fileName + " was not found", e);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("Failed to delete backup", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource loadFileAsResource(String fileName) {
|
||||
Assert.hasText(fileName, "Backup file name must not be blank");
|
||||
|
||||
// Get backup file path
|
||||
Path backupFilePath = Paths.get(haloProperties.getBackupDir(), fileName).normalize();
|
||||
try {
|
||||
// Build url resource
|
||||
Resource backupResource = new UrlResource(backupFilePath.toUri());
|
||||
if (!backupResource.exists()) {
|
||||
// If the backup resouce is not exist
|
||||
throw new NotFoundException("The file " + fileName + " was not found");
|
||||
}
|
||||
// Return the backup resource
|
||||
return backupResource;
|
||||
} catch (MalformedURLException e) {
|
||||
throw new NotFoundException("The file " + fileName + " was not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds backup dto.
|
||||
*
|
||||
|
@ -226,7 +248,7 @@ public class BackupServiceImpl implements BackupService {
|
|||
String backupFileName = backupPath.getFileName().toString();
|
||||
BackupDTO backup = new BackupDTO();
|
||||
backup.setDownloadUrl(buildDownloadUrl(backupFileName));
|
||||
backup.setDownloadLink(backup.getDownloadLink());
|
||||
backup.setDownloadLink(backup.getDownloadUrl());
|
||||
backup.setFilename(backupFileName);
|
||||
try {
|
||||
backup.setUpdateTime(Files.getLastModifiedTime(backupPath).toMillis());
|
||||
|
@ -247,9 +269,6 @@ public class BackupServiceImpl implements BackupService {
|
|||
private String buildDownloadUrl(@NonNull String filename) {
|
||||
Assert.hasText(filename, "File name must not be blank");
|
||||
|
||||
return StringUtils.joinWith("/",
|
||||
optionService.getBlogBaseUrl(),
|
||||
StringUtils.removeEnd(StringUtils.removeStart(haloProperties.getBackupUrlPrefix(), "/"), "/"),
|
||||
filename);
|
||||
return HaloUtils.compositeHttpUrl(optionService.getBlogBaseUrl(), "api/admin/backups/halo", filename);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,75 @@ public class HaloUtils {
|
|||
|
||||
private static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
|
||||
|
||||
public static final String URL_SEPARATOR = "/";
|
||||
|
||||
@NonNull
|
||||
public static String ensureBoth(@NonNull String string, @NonNull String bothfix) {
|
||||
return ensureBoth(string, bothfix, bothfix);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String ensureBoth(@NonNull String string, @NonNull String prefix, @NonNull String suffix) {
|
||||
return ensureSuffix(ensurePrefix(string, prefix), suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the string contain prefix.
|
||||
*
|
||||
* @param string string must not be blank
|
||||
* @param prefix prefix must not be blank
|
||||
* @return string contain prefix specified
|
||||
*/
|
||||
@NonNull
|
||||
public static String ensurePrefix(@NonNull String string, @NonNull String prefix) {
|
||||
Assert.hasText(string, "String must not be blank");
|
||||
Assert.hasText(prefix, "Prefix must not be blank");
|
||||
|
||||
return prefix + StringUtils.removeStart(string, prefix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensures the string contain suffix.
|
||||
*
|
||||
* @param string string must not be blank
|
||||
* @param suffix suffix must not be blank
|
||||
* @return string contain suffix specified
|
||||
*/
|
||||
@NonNull
|
||||
public static String ensureSuffix(@NonNull String string, @NonNull String suffix) {
|
||||
Assert.hasText(string, "String must not be blank");
|
||||
Assert.hasText(suffix, "Suffix must not be blank");
|
||||
|
||||
return StringUtils.removeEnd(string, suffix) + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composites partial url to full http url.
|
||||
*
|
||||
* @param partUrls partial urls must not be empty
|
||||
* @return full url
|
||||
*/
|
||||
public static String compositeHttpUrl(@NonNull String... partUrls) {
|
||||
Assert.notEmpty(partUrls, "Partial url must not be blank");
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < partUrls.length; i++) {
|
||||
String partUrl = partUrls[i];
|
||||
if (StringUtils.isBlank(partUrl)) {
|
||||
continue;
|
||||
}
|
||||
partUrl = StringUtils.removeStart(partUrl, URL_SEPARATOR);
|
||||
partUrl = StringUtils.removeEnd(partUrl, URL_SEPARATOR);
|
||||
if (i != 0) {
|
||||
builder.append(URL_SEPARATOR);
|
||||
}
|
||||
builder.append(partUrl);
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Desensitizes the plain text.
|
||||
*
|
||||
|
|
|
@ -2,11 +2,13 @@ package run.halo.app.utils;
|
|||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
|
@ -123,4 +125,22 @@ public class HaloUtilsTest {
|
|||
String plainText = " ";
|
||||
HaloUtils.desensitize(plainText, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeHttpUrl() {
|
||||
String url = HaloUtils.compositeHttpUrl("https://halo.run", "path1", "path2");
|
||||
assertEquals("https://halo.run/path1/path2", url);
|
||||
|
||||
url = HaloUtils.compositeHttpUrl("https://halo.run/", "path1", "path2");
|
||||
assertEquals("https://halo.run/path1/path2", url);
|
||||
|
||||
url = HaloUtils.compositeHttpUrl("https://halo.run/", "/path1", "path2");
|
||||
assertEquals("https://halo.run/path1/path2", url);
|
||||
|
||||
url = HaloUtils.compositeHttpUrl("https://halo.run/", "/path1/", "path2");
|
||||
assertEquals("https://halo.run/path1/path2", url);
|
||||
|
||||
url = HaloUtils.compositeHttpUrl("https://halo.run/", "/path1/", "/path2/");
|
||||
assertEquals("https://halo.run/path1/path2", url);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue