Fix backup download error and add temp token support

pull/755/head
johnniang 2019-11-30 03:39:03 +08:00
parent 5a0eac4722
commit 60cac1313e
12 changed files with 228 additions and 106 deletions

View File

@ -51,7 +51,8 @@ public class HaloConfiguration {
} }
@Bean @Bean
public RestTemplate httpsRestTemplate(RestTemplateBuilder builder) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { public RestTemplate httpsRestTemplate(RestTemplateBuilder builder)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
RestTemplate httpsRestTemplate = builder.build(); RestTemplate httpsRestTemplate = builder.build();
httpsRestTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(HttpClientUtils.createHttpsClient( httpsRestTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(HttpClientUtils.createHttpsClient(
(int) haloProperties.getDownloadTimeout().toMillis()))); (int) haloProperties.getDownloadTimeout().toMillis())));
@ -97,8 +98,9 @@ public class HaloConfiguration {
@Bean @Bean
public FilterRegistrationBean<ContentFilter> contentFilter(HaloProperties haloProperties, public FilterRegistrationBean<ContentFilter> contentFilter(HaloProperties haloProperties,
OptionService optionService) { OptionService optionService,
ContentFilter contentFilter = new ContentFilter(haloProperties, optionService); StringCacheStore cacheStore) {
ContentFilter contentFilter = new ContentFilter(haloProperties, optionService, cacheStore);
contentFilter.setFailureHandler(new ContentAuthenticationFailureHandler()); contentFilter.setFailureHandler(new ContentAuthenticationFailureHandler());
contentFilter.addExcludeUrlPatterns("/api/**", "/install", "/version", "/admin/**", "/js/**", "/css/**"); contentFilter.addExcludeUrlPatterns("/api/**", "/install", "/version", "/admin/**", "/js/**", "/css/**");
@ -113,8 +115,9 @@ public class HaloConfiguration {
@Bean @Bean
public FilterRegistrationBean<ApiAuthenticationFilter> apiAuthenticationFilter(HaloProperties haloProperties, public FilterRegistrationBean<ApiAuthenticationFilter> apiAuthenticationFilter(HaloProperties haloProperties,
ObjectMapper objectMapper, ObjectMapper objectMapper,
OptionService optionService) { OptionService optionService,
ApiAuthenticationFilter apiFilter = new ApiAuthenticationFilter(haloProperties, optionService); StringCacheStore cacheStore) {
ApiAuthenticationFilter apiFilter = new ApiAuthenticationFilter(haloProperties, optionService, cacheStore);
apiFilter.addExcludeUrlPatterns( apiFilter.addExcludeUrlPatterns(
"/api/content/*/comments", "/api/content/*/comments",
"/api/content/**/comments/**", "/api/content/**/comments/**",

View File

@ -13,6 +13,7 @@ import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
import run.halo.app.model.entity.User; import run.halo.app.model.entity.User;
import run.halo.app.model.support.HaloConst;
import run.halo.app.security.filter.AdminAuthenticationFilter; import run.halo.app.security.filter.AdminAuthenticationFilter;
import run.halo.app.security.filter.ApiAuthenticationFilter; import run.halo.app.security.filter.ApiAuthenticationFilter;
import run.halo.app.security.support.UserDetail; import run.halo.app.security.support.UserDetail;
@ -33,7 +34,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static run.halo.app.model.support.HaloConst.HALO_VERSION; import static run.halo.app.model.support.HaloConst.*;
import static springfox.documentation.schema.AlternateTypeRules.newRule; import static springfox.documentation.schema.AlternateTypeRules.newRule;
/** /**
@ -123,8 +124,8 @@ public class SwaggerConfiguration {
private List<ApiKey> adminApiKeys() { private List<ApiKey> adminApiKeys() {
return Arrays.asList( return Arrays.asList(
new ApiKey("Token from header", AdminAuthenticationFilter.ADMIN_TOKEN_HEADER_NAME, In.HEADER.name()), new ApiKey("Token from header", ADMIN_TOKEN_HEADER_NAME, In.HEADER.name()),
new ApiKey("Token from query", AdminAuthenticationFilter.ADMIN_TOKEN_QUERY_NAME, In.QUERY.name()) new ApiKey("Token from query", ADMIN_TOKEN_QUERY_NAME, In.QUERY.name())
); );
} }
@ -139,8 +140,8 @@ public class SwaggerConfiguration {
private List<ApiKey> contentApiKeys() { private List<ApiKey> contentApiKeys() {
return Arrays.asList( return Arrays.asList(
new ApiKey("Access key from header", ApiAuthenticationFilter.API_ACCESS_KEY_HEADER_NAME, In.HEADER.name()), new ApiKey("Access key from header", API_ACCESS_KEY_HEADER_NAME, In.HEADER.name()),
new ApiKey("Access key from query", ApiAuthenticationFilter.API_ACCESS_KEY_QUERY_NAME, In.QUERY.name()) new ApiKey("Access key from query", API_ACCESS_KEY_QUERY_NAME, In.QUERY.name())
); );
} }

View File

@ -14,6 +14,9 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import static run.halo.app.model.support.HaloConst.ADMIN_TOKEN_HEADER_NAME;
import static run.halo.app.model.support.HaloConst.API_ACCESS_KEY_HEADER_NAME;
/** /**
* Filter for CORS. * Filter for CORS.
* *
@ -21,7 +24,7 @@ import java.io.IOException;
*/ */
public class CorsFilter extends GenericFilterBean { public class CorsFilter extends GenericFilterBean {
private final static String ALLOW_HEADERS = StringUtils.joinWith(",", HttpHeaders.CONTENT_TYPE, AdminAuthenticationFilter.ADMIN_TOKEN_HEADER_NAME); private final static String ALLOW_HEADERS = StringUtils.joinWith(",", HttpHeaders.CONTENT_TYPE, ADMIN_TOKEN_HEADER_NAME, API_ACCESS_KEY_HEADER_NAME);
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

View File

@ -1,6 +1,9 @@
package run.halo.app.model.support; package run.halo.app.model.support;
import org.springframework.http.HttpHeaders;
import java.io.File; import java.io.File;
import java.time.Duration;
/** /**
* <pre> * <pre>
@ -106,6 +109,33 @@ public class HaloConst {
public final static String HALO_ADMIN_RELATIVE_BACKUP_PATH = "templates/admin-backup/"; public final static String HALO_ADMIN_RELATIVE_BACKUP_PATH = "templates/admin-backup/";
/**
* Content token header name.
*/
public final static String API_ACCESS_KEY_HEADER_NAME = "API-" + HttpHeaders.AUTHORIZATION;
/**
* Admin token header name.
*/
public final static String ADMIN_TOKEN_HEADER_NAME = "ADMIN-" + HttpHeaders.AUTHORIZATION;
/**
* Admin token param name.
*/
public final static String ADMIN_TOKEN_QUERY_NAME = "admin_token";
/**
* Temporary token.
*/
public final static String TEMP_TOKEN = "temp_token";
/**
* Content api token param name
*/
public final static String API_ACCESS_KEY_QUERY_NAME = "api_access_key";
public final static Duration TEMP_TOKEN_EXPIRATION = Duration.ofDays(7);
static { static {
// Set version // Set version
HALO_VERSION = HaloConst.class.getPackage().getImplementationVersion(); HALO_VERSION = HaloConst.class.getPackage().getImplementationVersion();

View File

@ -1,16 +1,21 @@
package run.halo.app.security.filter; package run.halo.app.security.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import run.halo.app.cache.StringCacheStore;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.NotInstallException; import run.halo.app.exception.NotInstallException;
import run.halo.app.model.properties.PrimaryProperties; import run.halo.app.model.properties.PrimaryProperties;
import run.halo.app.model.support.HaloConst;
import run.halo.app.security.context.SecurityContextHolder; import run.halo.app.security.context.SecurityContextHolder;
import run.halo.app.security.handler.AuthenticationFailureHandler; import run.halo.app.security.handler.AuthenticationFailureHandler;
import run.halo.app.security.handler.DefaultAuthenticationFailureHandler; import run.halo.app.security.handler.DefaultAuthenticationFailureHandler;
import run.halo.app.security.util.SecurityUtils;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@ -18,10 +23,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/** /**
* Abstract authentication filter. * Abstract authentication filter.
@ -29,21 +31,29 @@ import java.util.Set;
* @author johnniang * @author johnniang
* @date 19-4-16 * @date 19-4-16
*/ */
@Slf4j
public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter { public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter {
protected final AntPathMatcher antPathMatcher; protected final AntPathMatcher antPathMatcher;
protected final HaloProperties haloProperties; protected final HaloProperties haloProperties;
protected final OptionService optionService; protected final OptionService optionService;
protected final StringCacheStore cacheStore;
private AuthenticationFailureHandler failureHandler; private AuthenticationFailureHandler failureHandler;
/** /**
* Exclude url patterns. * Exclude url patterns.
*/ */
private Set<String> excludeUrlPatterns = new HashSet<>(2); private Set<String> excludeUrlPatterns = new HashSet<>(2);
protected AbstractAuthenticationFilter(HaloProperties haloProperties, AbstractAuthenticationFilter(HaloProperties haloProperties,
OptionService optionService) { OptionService optionService,
StringCacheStore cacheStore) {
this.haloProperties = haloProperties; this.haloProperties = haloProperties;
this.optionService = optionService; this.optionService = optionService;
this.cacheStore = cacheStore;
antPathMatcher = new AntPathMatcher(); antPathMatcher = new AntPathMatcher();
} }
@ -146,6 +156,11 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter
return; return;
} }
if (checkForTempToken(request)) {
filterChain.doFilter(request, response);
return;
}
try { try {
// Do authenticate // Do authenticate
doAuthenticate(request, response, filterChain); doAuthenticate(request, response, filterChain);
@ -154,4 +169,58 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter
} }
} }
private boolean checkForTempToken(HttpServletRequest request) {
// Get token from request
String tempToken = getTokenFromRequest(request, HaloConst.TEMP_TOKEN, HaloConst.TEMP_TOKEN);
if (StringUtils.isEmpty(tempToken)) {
return false;
}
String tempTokenKey = SecurityUtils.buildTempTokenKey(tempToken);
// Check the token
Optional<Integer> tokenCountOptional = cacheStore.getAny(tempTokenKey, Integer.class);
if (!tokenCountOptional.isPresent()) {
// If the token is not found
return false;
}
log.info("Got valid temp token: [{}]", tempToken);
int count = tokenCountOptional.get();
// TODO May cause unsafe thread, fixing next time
// Count down
count--;
if (count <= 0) {
// If count is less than 0, then clear this temp token
cacheStore.delete(tempTokenKey);
} else {
// Put the less count
cacheStore.put(tempTokenKey, String.valueOf(count));
}
return true;
}
String getTokenFromRequest(@NonNull HttpServletRequest request, @NonNull String tokenQueryName, @NonNull String tokenHeaderName) {
Assert.notNull(request, "Http servlet request must not be null");
Assert.hasText(tokenQueryName, "Token query name must not be blank");
Assert.hasText(tokenHeaderName, "Token header name must not be blank");
// Get from header
String accessKey = request.getHeader(tokenHeaderName);
// Get from param
if (StringUtils.isBlank(accessKey)) {
accessKey = request.getParameter(tokenQueryName);
log.debug("Got access key from parameter: [{}: {}]", tokenQueryName, accessKey);
} else {
log.debug("Got access key from header: [{}: {}]", tokenHeaderName, accessKey);
}
return accessKey;
}
} }

View File

@ -2,9 +2,7 @@ package run.halo.app.security.filter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import run.halo.app.cache.StringCacheStore; import run.halo.app.cache.StringCacheStore;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.AuthenticationException; import run.halo.app.exception.AuthenticationException;
@ -24,6 +22,9 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import static run.halo.app.model.support.HaloConst.ADMIN_TOKEN_HEADER_NAME;
import static run.halo.app.model.support.HaloConst.ADMIN_TOKEN_QUERY_NAME;
/** /**
* Admin authentication filter. * Admin authentication filter.
* *
@ -32,34 +33,8 @@ import java.util.Optional;
@Slf4j @Slf4j
public class AdminAuthenticationFilter extends AbstractAuthenticationFilter { public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {
/**
* Admin session key.
*/
public final static String ADMIN_SESSION_KEY = "halo.admin.session";
/**
* Access token cache prefix.
*/
public final static String TOKEN_ACCESS_CACHE_PREFIX = "halo.admin.access.token.";
/**
* Refresh token cache prefix.
*/
public final static String TOKEN_REFRESH_CACHE_PREFIX = "halo.admin.refresh.token.";
/**
* Admin token header name.
*/
public final static String ADMIN_TOKEN_HEADER_NAME = "ADMIN-" + HttpHeaders.AUTHORIZATION;
/**
* Admin token param name.
*/
public final static String ADMIN_TOKEN_QUERY_NAME = "admin_token";
private final HaloProperties haloProperties; private final HaloProperties haloProperties;
private final StringCacheStore cacheStore;
private final UserService userService; private final UserService userService;
@ -67,8 +42,7 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {
UserService userService, UserService userService,
HaloProperties haloProperties, HaloProperties haloProperties,
OptionService optionService) { OptionService optionService) {
super(haloProperties, optionService); super(haloProperties, optionService, cacheStore);
this.cacheStore = cacheStore;
this.userService = userService; this.userService = userService;
this.haloProperties = haloProperties; this.haloProperties = haloProperties;
} }
@ -117,21 +91,7 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {
@Override @Override
protected String getTokenFromRequest(@NonNull HttpServletRequest request) { protected String getTokenFromRequest(@NonNull HttpServletRequest request) {
Assert.notNull(request, "Http servlet request must not be null"); return getTokenFromRequest(request, ADMIN_TOKEN_QUERY_NAME, ADMIN_TOKEN_HEADER_NAME);
// Get from header
String token = request.getHeader(ADMIN_TOKEN_HEADER_NAME);
// Get from param
if (StringUtils.isBlank(token)) {
token = request.getParameter(ADMIN_TOKEN_QUERY_NAME);
log.debug("Got token from parameter: [{}: {}]", ADMIN_TOKEN_QUERY_NAME, token);
} else {
log.debug("Got token from header: [{}: {}]", ADMIN_TOKEN_HEADER_NAME, token);
}
return token;
} }
} }

View File

@ -2,9 +2,8 @@ package run.halo.app.security.filter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.util.Assert; import run.halo.app.cache.StringCacheStore;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.AuthenticationException; import run.halo.app.exception.AuthenticationException;
import run.halo.app.exception.ForbiddenException; import run.halo.app.exception.ForbiddenException;
@ -19,6 +18,8 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import static run.halo.app.model.support.HaloConst.*;
/** /**
* Api authentication Filter * Api authentication Filter
* *
@ -27,15 +28,12 @@ import java.util.Optional;
@Slf4j @Slf4j
public class ApiAuthenticationFilter extends AbstractAuthenticationFilter { public class ApiAuthenticationFilter extends AbstractAuthenticationFilter {
public final static String API_ACCESS_KEY_HEADER_NAME = "API-" + HttpHeaders.AUTHORIZATION;
public final static String API_ACCESS_KEY_QUERY_NAME = "api_access_key";
private final OptionService optionService; private final OptionService optionService;
public ApiAuthenticationFilter(HaloProperties haloProperties, public ApiAuthenticationFilter(HaloProperties haloProperties,
OptionService optionService) { OptionService optionService,
super(haloProperties, optionService); StringCacheStore cacheStore) {
super(haloProperties, optionService, cacheStore);
this.optionService = optionService; this.optionService = optionService;
} }
@ -95,25 +93,10 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter {
} }
} }
return result; return result;
} }
@Override @Override
protected String getTokenFromRequest(@NonNull HttpServletRequest request) { protected String getTokenFromRequest(@NonNull HttpServletRequest request) {
Assert.notNull(request, "Http servlet request must not be null"); return getTokenFromRequest(request, ADMIN_TOKEN_QUERY_NAME, ADMIN_TOKEN_HEADER_NAME);
// Get from header
String accessKey = request.getHeader(API_ACCESS_KEY_HEADER_NAME);
// Get from param
if (StringUtils.isBlank(accessKey)) {
accessKey = request.getParameter(API_ACCESS_KEY_QUERY_NAME);
log.debug("Got access key from parameter: [{}: {}]", API_ACCESS_KEY_QUERY_NAME, accessKey);
} else {
log.debug("Got access key from header: [{}: {}]", API_ACCESS_KEY_HEADER_NAME, accessKey);
}
return accessKey;
} }
} }

View File

@ -1,5 +1,6 @@
package run.halo.app.security.filter; package run.halo.app.security.filter;
import run.halo.app.cache.StringCacheStore;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
@ -17,8 +18,10 @@ import java.io.IOException;
*/ */
public class ContentFilter extends AbstractAuthenticationFilter { public class ContentFilter extends AbstractAuthenticationFilter {
public ContentFilter(HaloProperties haloProperties, OptionService optionService) { public ContentFilter(HaloProperties haloProperties,
super(haloProperties, optionService); OptionService optionService,
StringCacheStore cacheStore) {
super(haloProperties, optionService, cacheStore);
} }
@Override @Override

View File

@ -4,11 +4,6 @@ import org.springframework.lang.NonNull;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import run.halo.app.model.entity.User; import run.halo.app.model.entity.User;
import static run.halo.app.security.filter.AdminAuthenticationFilter.TOKEN_ACCESS_CACHE_PREFIX;
import static run.halo.app.security.filter.AdminAuthenticationFilter.TOKEN_REFRESH_CACHE_PREFIX;
import static run.halo.app.service.AdminService.ACCESS_TOKEN_CACHE_PREFIX;
import static run.halo.app.service.AdminService.REFRESH_TOKEN_CACHE_PREFIX;
/** /**
* Security utilities. * Security utilities.
* *
@ -17,6 +12,23 @@ import static run.halo.app.service.AdminService.REFRESH_TOKEN_CACHE_PREFIX;
*/ */
public class SecurityUtils { public class SecurityUtils {
/**
* Access token cache prefix.
*/
public final static String TOKEN_ACCESS_CACHE_PREFIX = "halo.admin.access.token.";
/**
* Refresh token cache prefix.
*/
public final static String TOKEN_REFRESH_CACHE_PREFIX = "halo.admin.refresh.token.";
public final static String ACCESS_TOKEN_CACHE_PREFIX = "halo.admin.access_token.";
public final static String REFRESH_TOKEN_CACHE_PREFIX = "halo.admin.refresh_token.";
public final static String TEMP_TOKEN_CACHE_PREFIX = "halo.temp.token.";
private SecurityUtils() { private SecurityUtils() {
} }
@ -47,4 +59,11 @@ public class SecurityUtils {
return TOKEN_REFRESH_CACHE_PREFIX + refreshToken; return TOKEN_REFRESH_CACHE_PREFIX + refreshToken;
} }
@NonNull
public static String buildTempTokenKey(@NonNull String tempToken) {
Assert.hasText(tempToken, "Temporary token must not be blank");
return TEMP_TOKEN_CACHE_PREFIX + tempToken;
}
} }

View File

@ -23,10 +23,6 @@ public interface AdminService {
int REFRESH_TOKEN_EXPIRED_DAYS = 30; int REFRESH_TOKEN_EXPIRED_DAYS = 30;
String ACCESS_TOKEN_CACHE_PREFIX = "halo.admin.access_token.";
String REFRESH_TOKEN_CACHE_PREFIX = "halo.admin.refresh_token.";
String LOGS_PATH = "logs/spring.log"; String LOGS_PATH = "logs/spring.log";
/** /**

View File

@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.http.client.utils.URIBuilder;
import org.json.JSONObject; import org.json.JSONObject;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource; import org.springframework.core.io.UrlResource;
@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import run.halo.app.cache.StringCacheStore;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.NotFoundException; import run.halo.app.exception.NotFoundException;
import run.halo.app.exception.ServiceException; import run.halo.app.exception.ServiceException;
@ -22,6 +24,7 @@ import run.halo.app.model.dto.post.BasePostDetailDTO;
import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Post;
import run.halo.app.model.entity.Tag; import run.halo.app.model.entity.Tag;
import run.halo.app.model.support.HaloConst; import run.halo.app.model.support.HaloConst;
import run.halo.app.security.util.SecurityUtils;
import run.halo.app.service.BackupService; import run.halo.app.service.BackupService;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.service.PostService; import run.halo.app.service.PostService;
@ -31,6 +34,7 @@ import run.halo.app.utils.HaloUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
@ -42,9 +46,12 @@ import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static run.halo.app.model.support.HaloConst.*;
/** /**
* Backup service implementation. * Backup service implementation.
* *
@ -57,18 +64,23 @@ import java.util.stream.Stream;
public class BackupServiceImpl implements BackupService { public class BackupServiceImpl implements BackupService {
private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static final String BACKUP_TOKEN_KEY_PREFIX = "backup-token-";
private final PostService postService; private final PostService postService;
private final PostTagService postTagService; private final PostTagService postTagService;
private final OptionService optionService; private final OptionService optionService;
private final StringCacheStore cacheStore;
private final HaloProperties haloProperties; private final HaloProperties haloProperties;
public BackupServiceImpl(PostService postService, public BackupServiceImpl(PostService postService,
PostTagService postTagService, PostTagService postTagService,
OptionService optionService, OptionService optionService,
StringCacheStore stringCacheStore,
HaloProperties haloProperties) { HaloProperties haloProperties) {
this.postService = postService; this.postService = postService;
this.postTagService = postTagService; this.postTagService = postTagService;
this.optionService = optionService; this.optionService = optionService;
this.cacheStore = stringCacheStore;
this.haloProperties = haloProperties; this.haloProperties = haloProperties;
} }
@ -162,9 +174,8 @@ public class BackupServiceImpl implements BackupService {
try { try {
// Create zip path for halo zip // Create zip path for halo zip
String haloZipFileName = HaloConst.HALO_BACKUP_PREFIX + String haloZipFileName = HaloConst.HALO_BACKUP_PREFIX +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-")) +
IdUtil.simpleUUID() + IdUtil.simpleUUID().hashCode() + ".zip";
".zip";
// Create halo zip file // Create halo zip file
Path haloZipPath = Files.createFile(Paths.get(haloProperties.getBackupDir(), haloZipFileName)); Path haloZipPath = Files.createFile(Paths.get(haloProperties.getBackupDir(), haloZipFileName));
@ -247,14 +258,16 @@ public class BackupServiceImpl implements BackupService {
String backupFileName = backupPath.getFileName().toString(); String backupFileName = backupPath.getFileName().toString();
BackupDTO backup = new BackupDTO(); BackupDTO backup = new BackupDTO();
backup.setDownloadUrl(buildDownloadUrl(backupFileName));
backup.setDownloadLink(backup.getDownloadUrl());
backup.setFilename(backupFileName);
try { try {
backup.setDownloadUrl(buildDownloadUrl(backupFileName));
backup.setDownloadLink(backup.getDownloadUrl());
backup.setFilename(backupFileName);
backup.setUpdateTime(Files.getLastModifiedTime(backupPath).toMillis()); backup.setUpdateTime(Files.getLastModifiedTime(backupPath).toMillis());
backup.setFileSize(Files.size(backupPath)); backup.setFileSize(Files.size(backupPath));
} catch (IOException e) { } catch (IOException e) {
throw new ServiceException("Failed to access file " + backupPath.toString(), e); throw new ServiceException("Failed to access file " + backupPath, e);
} catch (URISyntaxException e) {
throw new ServiceException("Failed to generate download link for file: " + backupPath, e);
} }
return backup; return backup;
@ -266,9 +279,36 @@ public class BackupServiceImpl implements BackupService {
* @param filename filename must not be blank * @param filename filename must not be blank
* @return download url * @return download url
*/ */
private String buildDownloadUrl(@NonNull String filename) { private String buildDownloadUrl(@NonNull String filename) throws URISyntaxException {
Assert.hasText(filename, "File name must not be blank"); Assert.hasText(filename, "File name must not be blank");
return HaloUtils.compositeHttpUrl(optionService.getBlogBaseUrl(), "api/admin/backups/halo", filename); // Composite http url
String backupFullUrl = HaloUtils.compositeHttpUrl(optionService.getBlogBaseUrl(), "api/admin/backups/halo", filename);
// Get temp token
String tempToken = cacheStore.get(buildBackupTokenKey(filename)).orElseGet(() -> {
String token = buildTempToken(1);
// Cache this projection
cacheStore.putIfAbsent(buildBackupTokenKey(filename), token, TEMP_TOKEN_EXPIRATION.toDays(), TimeUnit.DAYS);
return token;
});
return new URIBuilder(backupFullUrl).addParameter(TEMP_TOKEN, tempToken).toString();
}
private String buildBackupTokenKey(String backupFileName) {
return BACKUP_TOKEN_KEY_PREFIX + backupFileName;
}
private String buildTempToken(@NonNull Object value) {
Assert.notNull(value, "Temp token value must not be null");
// Generate temp token
String tempToken = HaloUtils.randomUUIDWithoutDash();
// Cache the token
cacheStore.putIfAbsent(SecurityUtils.buildTempTokenKey(tempToken), value.toString(), TEMP_TOKEN_EXPIRATION.toDays(), TimeUnit.DAYS);
return tempToken;
} }
} }

View File

@ -5,9 +5,13 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR; import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR;
@ -26,6 +30,17 @@ public class HaloUtils {
public static final String URL_SEPARATOR = "/"; public static final String URL_SEPARATOR = "/";
/**
* Gets current http servlet request.
*
* @return current http servlet request
*/
@NonNull
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))
.getRequest();
}
@NonNull @NonNull
public static String ensureBoth(@NonNull String string, @NonNull String bothfix) { public static String ensureBoth(@NonNull String string, @NonNull String bothfix) {
return ensureBoth(string, bothfix, bothfix); return ensureBoth(string, bothfix, bothfix);