From 9a0ebdad254fb8c71dfed0a546535a36c821bf6b Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:45:29 +0800 Subject: [PATCH] refactor: redirect to original image if thumbnail is inaccessible (#6556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.19.x #### What this PR does / why we need it: 获取缩略图时检查缩略图链接是否可访问否则重定向到原图链接 #### Does this PR introduce a user-facing change? ```release-note 获取缩略图时检查缩略图链接是否可访问否则重定向到原图链接 ``` --- .../impl/LocalThumbnailServiceImpl.java | 4 +- .../app/theme/endpoint/ThumbnailEndpoint.java | 24 ++++++++- .../theme/endpoint/ThumbnailEndpointTest.java | 51 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 application/src/test/java/run/halo/app/theme/endpoint/ThumbnailEndpointTest.java diff --git a/application/src/main/java/run/halo/app/core/attachment/impl/LocalThumbnailServiceImpl.java b/application/src/main/java/run/halo/app/core/attachment/impl/LocalThumbnailServiceImpl.java index d45f18f9e..747606253 100644 --- a/application/src/main/java/run/halo/app/core/attachment/impl/LocalThumbnailServiceImpl.java +++ b/application/src/main/java/run/halo/app/core/attachment/impl/LocalThumbnailServiceImpl.java @@ -45,6 +45,7 @@ import run.halo.app.extension.Metadata; import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.infra.exception.NotFoundException; @Slf4j @Component @@ -76,7 +77,8 @@ public class LocalThumbnailServiceImpl implements LocalThumbnailService { @Override public Mono getOriginalImageUri(URI thumbnailUri) { return fetchThumbnail(thumbnailUri) - .map(local -> URI.create(local.getSpec().getImageUri())); + .map(local -> URI.create(local.getSpec().getImageUri())) + .switchIfEmpty(Mono.error(() -> new NotFoundException("Resource not found."))); } @Override diff --git a/application/src/main/java/run/halo/app/theme/endpoint/ThumbnailEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/ThumbnailEndpoint.java index 13594ea13..aeb332b0d 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/ThumbnailEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/ThumbnailEndpoint.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.io.IOException; import java.net.URI; import java.time.Instant; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.operation.Builder; @@ -17,9 +18,11 @@ import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; @@ -41,6 +44,7 @@ import run.halo.app.extension.GroupVersion; @Component @RequiredArgsConstructor public class ThumbnailEndpoint implements CustomEndpoint { + private final WebClient webClient = WebClient.builder().build(); private final LocalThumbnailService localThumbnailService; private final WebProperties webProperties; private final ThumbnailService thumbnailService; @@ -63,8 +67,26 @@ public class ThumbnailEndpoint implements CustomEndpoint { private Mono getThumbnailByUri(ServerRequest request) { var query = new ThumbnailQuery(request.queryParams()); return thumbnailService.generate(query.getUri(), query.getSize()) + .filterWhen(uri -> isAccessible(request, uri)) .defaultIfEmpty(query.getUri()) - .flatMap(uri -> ServerResponse.permanentRedirect(uri).build()); + .flatMap(uri -> ServerResponse.temporaryRedirect(uri).build()); + } + + Mono isAccessible(ServerRequest request, URI uri) { + var url = Optional.of(uri) + .filter(URI::isAbsolute) + .orElseGet(() -> request.uriBuilder().replacePath(uri.toASCIIString()).build()); + // resource handler does not support head access for Halo, so use get request here + return webClient.get() + .uri(url) + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range + .header(HttpHeaders.RANGE, "bytes=0-0") + .exchangeToMono(response -> { + var statusCode = response.statusCode(); + return Mono.just(statusCode.is2xxSuccessful() || statusCode.is3xxRedirection()); + }) + .onErrorReturn(false) + .defaultIfEmpty(false); } static class ThumbnailQuery { diff --git a/application/src/test/java/run/halo/app/theme/endpoint/ThumbnailEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/ThumbnailEndpointTest.java new file mode 100644 index 000000000..18f3414e8 --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/endpoint/ThumbnailEndpointTest.java @@ -0,0 +1,51 @@ +package run.halo.app.theme.endpoint; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.net.URI; +import org.junit.jupiter.api.BeforeEach; +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.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Mono; +import run.halo.app.core.attachment.ThumbnailService; + +/** + * Tests for {@link ThumbnailEndpoint}. + * + * @author guqing + * @since 2.19.0 + */ +@ExtendWith(MockitoExtension.class) +class ThumbnailEndpointTest { + + WebTestClient webClient; + + @Mock + private ThumbnailService thumbnailService; + + @InjectMocks + private ThumbnailEndpoint endpoint; + + @BeforeEach + void setUp() { + webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()) + .build(); + } + + @Test + void thumbnailUriNotAccessible() { + when(thumbnailService.generate(any(), any())) + .thenReturn(Mono.just(URI.create("/thumbnail-not-found.png"))); + webClient.get() + .uri("/thumbnails/-/via-uri?size=l&uri=/myavatar.png") + .exchange() + .expectAll(responseSpec -> responseSpec.expectHeader().location("/myavatar.png")) + .expectStatus() + .is3xxRedirection(); + } +}