Fix the problem of crashing requests when slug names of single page contains special chars (#4013)

#### What type of PR is this?

/kind bug
/area core
/milestone 2.6.x

#### What this PR does / why we need it:

This PR refactors request predicate of path when building router functions for single page. I only compare the exact slug name instead of treating it as a URI template.

See <https://github.com/halo-dev/halo/issues/3931> for more.

#### Which issue(s) this PR fixes:

Fixes <https://github.com/halo-dev/halo/issues/3931>

#### Special notes for your reviewer:

1. Try to create a single page with slug name like `{}[]{[]}[{}]`.
2. Publish the single page.
3. Try to request the page.
4. See the result.

#### Does this PR introduce a user-facing change?

```release-note
修复页面别名包含特殊字符导致无法访问的问题
```
pull/4014/head^2
John Niang 2023-05-31 12:21:00 +08:00 committed by GitHub
parent ee1ea06171
commit 4c2e8410b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 60 additions and 4 deletions

View File

@ -1,24 +1,29 @@
package run.halo.app.theme.router;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.methods;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.SinglePage;
@ -41,7 +46,7 @@ import run.halo.app.theme.finders.SinglePageFinder;
@RequiredArgsConstructor
public class SinglePageRoute
implements RouterFunction<ServerResponse>, Reconciler<Reconciler.Request>, DisposableBean {
private final Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap =
private Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap =
new ConcurrentHashMap<>();
private final ExtensionClient client;
@ -58,6 +63,15 @@ public class SinglePageRoute
.next();
}
/**
* Set quickRouteMap. This method is only for testing.
*
* @param quickRouteMap fresh quickRouteMap.
*/
void setQuickRouteMap(Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap) {
this.quickRouteMap = quickRouteMap;
}
@Override
public void accept(@NonNull RouterFunctions.Visitor visitor) {
routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor));
@ -66,14 +80,23 @@ public class SinglePageRoute
private List<RouterFunction<ServerResponse>> routerFunctions() {
return quickRouteMap.keySet().stream()
.map(nameSlugPair -> {
String routePath = singlePageRoute(nameSlugPair.slug());
return RouterFunctions.route(GET(routePath)
var routePath = singlePageRoute(nameSlugPair.slug());
return RouterFunctions.route(methods(HttpMethod.GET)
.and(exactPath(routePath))
.and(RequestPredicates.accept(MediaType.TEXT_HTML)),
handlerFunction(nameSlugPair.name()));
})
.collect(Collectors.toList());
}
private RequestPredicate exactPath(String path) {
return request -> {
var encodedRoutePath = UriUtils.encodePath(path, StandardCharsets.UTF_8);
var requestPath = request.requestPath().pathWithinApplication().value();
return Objects.equals(requestPath, encodedRoutePath);
};
}
@Override
public Result reconcile(Request request) {
client.fetch(SinglePage.class, request.name())

View File

@ -1,17 +1,24 @@
package run.halo.app.theme.router;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import java.net.URI;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.HandlerStrategies;
@ -20,13 +27,16 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.Metadata;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.finders.vo.SinglePageVo;
import run.halo.app.theme.router.SinglePageRoute.NameSlugPair;
/**
* Tests for {@link SinglePageRoute}.
@ -97,4 +107,27 @@ class SinglePageRouteTest {
.exchange()
.expectStatus().isOk();
}
@Test
void shouldNotThrowErrorIfSlugNameContainsSpecialChars() {
var specialChars = "/with-special-chars-{}-[]-{{}}-{[]}-[{}]";
var specialCharsUri =
URI.create(UriUtils.encodePath(specialChars, UTF_8));
var mockHttpRequest = MockServerHttpRequest.get(specialCharsUri.toString())
.accept(MediaType.TEXT_HTML)
.build();
var mockExchange = MockServerWebExchange.from(mockHttpRequest);
var request = MockServerRequest.builder()
.exchange(mockExchange)
.uri(specialCharsUri)
.method(HttpMethod.GET)
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE)
.build();
var nameSlugPair = new NameSlugPair("fake-single-page", specialChars);
singlePageRoute.setQuickRouteMap(Map.of(nameSlugPair, r -> ServerResponse.ok().build()));
StepVerifier.create(singlePageRoute.route(request))
.expectNextCount(1)
.verifyComplete();
}
}