Skip to content

Commit 136b33b

Browse files
committed
Allow serving static files from RouterFunctions
This commit adds the ability to serve Resources (static files) through a RouterFunction. Two methods have been added to RouterFunctions: one that exposes a given directory given a path pattern, and a generic method that requires a lookup function. Issue: SPR-14913
1 parent 20c6065 commit 136b33b

File tree

14 files changed

+671
-48
lines changed

14 files changed

+671
-48
lines changed

spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultServerResponseBuilder.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.LinkedHashSet;
2626
import java.util.Locale;
2727
import java.util.Map;
28+
import java.util.Set;
2829
import java.util.function.Supplier;
2930
import java.util.stream.Collectors;
3031
import java.util.stream.Stream;
@@ -55,12 +56,12 @@
5556
*/
5657
class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
5758

58-
private final int statusCode;
59+
private final HttpStatus statusCode;
5960

6061
private final HttpHeaders headers = new HttpHeaders();
6162

6263

63-
public DefaultServerResponseBuilder(int statusCode) {
64+
public DefaultServerResponseBuilder(HttpStatus statusCode) {
6465
this.statusCode = statusCode;
6566
}
6667

@@ -87,6 +88,12 @@ public ServerResponse.BodyBuilder allow(HttpMethod... allowedMethods) {
8788
return this;
8889
}
8990

91+
@Override
92+
public ServerResponse.BodyBuilder allow(Set<HttpMethod> allowedMethods) {
93+
this.headers.setAllow(allowedMethods);
94+
return this;
95+
}
96+
9097
@Override
9198
public ServerResponse.BodyBuilder contentLength(long contentLength) {
9299
this.headers.setContentLength(contentLength);
@@ -144,9 +151,7 @@ public ServerResponse.BodyBuilder varyBy(String... requestHeaders) {
144151

145152
@Override
146153
public ServerResponse<Void> build() {
147-
return body(BodyInserter.of(
148-
(response, context) -> response.setComplete(),
149-
() -> null));
154+
return body(BodyInserters.empty());
150155
}
151156

152157
@Override
@@ -194,20 +199,20 @@ private Map<String, Object> toModelMap(Object[] modelAttributes) {
194199
}
195200

196201

197-
private static abstract class AbstractServerResponse<T> implements ServerResponse<T> {
202+
static abstract class AbstractServerResponse<T> implements ServerResponse<T> {
198203

199-
private final int statusCode;
204+
private final HttpStatus statusCode;
200205

201206
private final HttpHeaders headers;
202207

203-
protected AbstractServerResponse(int statusCode, HttpHeaders headers) {
208+
protected AbstractServerResponse(HttpStatus statusCode, HttpHeaders headers) {
204209
this.statusCode = statusCode;
205210
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
206211
}
207212

208213
@Override
209214
public final HttpStatus statusCode() {
210-
return HttpStatus.valueOf(this.statusCode);
215+
return this.statusCode;
211216
}
212217

213218
@Override
@@ -216,7 +221,7 @@ public final HttpHeaders headers() {
216221
}
217222

218223
protected void writeStatusAndHeaders(ServerHttpResponse response) {
219-
response.setStatusCode(HttpStatus.valueOf(this.statusCode));
224+
response.setStatusCode(this.statusCode);
220225
HttpHeaders responseHeaders = response.getHeaders();
221226

222227
if (!this.headers.isEmpty()) {
@@ -233,7 +238,7 @@ private static final class BodyInserterServerResponse<T> extends AbstractServerR
233238

234239
private final BodyInserter<T, ? super ServerHttpResponse> inserter;
235240

236-
public BodyInserterServerResponse(int statusCode, HttpHeaders headers,
241+
public BodyInserterServerResponse(HttpStatus statusCode, HttpHeaders headers,
237242
BodyInserter<T, ? super ServerHttpResponse> inserter) {
238243

239244
super(statusCode, headers);
@@ -267,7 +272,9 @@ private static final class RenderingServerResponse extends AbstractServerRespons
267272

268273
private final Rendering rendering;
269274

270-
public RenderingServerResponse(int statusCode, HttpHeaders headers, String name, Map<String, Object> model) {
275+
public RenderingServerResponse(HttpStatus statusCode, HttpHeaders headers, String name,
276+
Map<String, Object> model) {
277+
271278
super(statusCode, headers);
272279
this.name = name;
273280
this.model = model;

spring-web-reactive/src/main/java/org/springframework/web/reactive/function/HandlerFilterFunction.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.web.reactive.function;
1818

19+
import java.util.function.Function;
20+
1921
import org.springframework.util.Assert;
2022
import org.springframework.web.reactive.function.support.ServerRequestWrapper;
2123

@@ -70,4 +72,31 @@ default HandlerFunction<R> apply(HandlerFunction<T> handler) {
7072
return request -> this.filter(request, handler);
7173
}
7274

75+
/**
76+
* Adapt the given request processor function to a filter function that only operates on the
77+
* {@code ClientRequest}.
78+
* @param requestProcessor the request processor
79+
* @return the filter adaptation of the request processor
80+
*/
81+
static HandlerFilterFunction<?, ?> ofRequestProcessor(Function<ServerRequest,
82+
ServerRequest> requestProcessor) {
83+
84+
Assert.notNull(requestProcessor, "'requestProcessor' must not be null");
85+
return (request, next) -> next.handle(requestProcessor.apply(request));
86+
}
87+
88+
/**
89+
* Adapt the given response processor function to a filter function that only operates on the
90+
* {@code ClientResponse}.
91+
* @param responseProcessor the response processor
92+
* @return the filter adaptation of the request processor
93+
*/
94+
static <T, R> HandlerFilterFunction<T, R> ofResponseProcessor(Function<ServerResponse<T>,
95+
ServerResponse<R>> responseProcessor) {
96+
97+
Assert.notNull(responseProcessor, "'responseProcessor' must not be null");
98+
return (request, next) -> responseProcessor.apply(next.handle(request));
99+
}
100+
101+
73102
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.reactive.function;
18+
19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Optional;
23+
import java.util.function.Function;
24+
25+
import org.springframework.core.io.ClassPathResource;
26+
import org.springframework.core.io.Resource;
27+
import org.springframework.core.io.UrlResource;
28+
import org.springframework.util.AntPathMatcher;
29+
import org.springframework.util.PathMatcher;
30+
import org.springframework.util.ResourceUtils;
31+
import org.springframework.util.StringUtils;
32+
import org.springframework.web.util.UriUtils;
33+
34+
/**
35+
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
36+
*
37+
* @author Arjen Poutsma
38+
* @since 5.0
39+
*/
40+
class PathResourceLookupFunction implements Function<ServerRequest, Optional<Resource>> {
41+
42+
private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
43+
44+
private final String pattern;
45+
46+
private final Resource location;
47+
48+
public PathResourceLookupFunction(String pattern, Resource location) {
49+
this.pattern = pattern;
50+
this.location = location;
51+
}
52+
53+
@Override
54+
public Optional<Resource> apply(ServerRequest request) {
55+
String path = processPath(request.path());
56+
if (path.contains("%")) {
57+
path = UriUtils.decode(path, StandardCharsets.UTF_8);
58+
}
59+
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
60+
return Optional.empty();
61+
}
62+
if (!PATH_MATCHER.match(this.pattern, path)) {
63+
return Optional.empty();
64+
}
65+
else {
66+
path = PATH_MATCHER.extractPathWithinPattern(this.pattern, path);
67+
}
68+
try {
69+
Resource resource = this.location.createRelative(path);
70+
if (resource.exists() && resource.isReadable() && isResourceUnderLocation(resource)) {
71+
return Optional.of(resource);
72+
}
73+
else {
74+
return Optional.empty();
75+
}
76+
}
77+
catch (IOException ex) {
78+
throw new UncheckedIOException(ex);
79+
}
80+
}
81+
82+
private static String processPath(String path) {
83+
boolean slash = false;
84+
for (int i = 0; i < path.length(); i++) {
85+
if (path.charAt(i) == '/') {
86+
slash = true;
87+
}
88+
else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
89+
if (i == 0 || (i == 1 && slash)) {
90+
return path;
91+
}
92+
path = slash ? "/" + path.substring(i) : path.substring(i);
93+
return path;
94+
}
95+
}
96+
return (slash ? "/" : "");
97+
}
98+
99+
private static boolean isInvalidPath(String path) {
100+
if (path.contains("WEB-INF") || path.contains("META-INF")) {
101+
return true;
102+
}
103+
if (path.contains(":/")) {
104+
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
105+
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
106+
return true;
107+
}
108+
}
109+
if (path.contains("..")) {
110+
path = StringUtils.cleanPath(path);
111+
if (path.contains("../")) {
112+
return true;
113+
}
114+
}
115+
return false;
116+
}
117+
118+
private boolean isResourceUnderLocation(Resource resource) throws
119+
IOException {
120+
if (resource.getClass() != this.location.getClass()) {
121+
return false;
122+
}
123+
124+
String resourcePath;
125+
String locationPath;
126+
127+
if (resource instanceof UrlResource) {
128+
resourcePath = resource.getURL().toExternalForm();
129+
locationPath = StringUtils.cleanPath(this.location.getURL().toString());
130+
}
131+
else if (resource instanceof ClassPathResource) {
132+
resourcePath = ((ClassPathResource) resource).getPath();
133+
locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath());
134+
}
135+
else {
136+
resourcePath = resource.getURL().getPath();
137+
locationPath = StringUtils.cleanPath(this.location.getURL().getPath());
138+
}
139+
140+
if (locationPath.equals(resourcePath)) {
141+
return true;
142+
}
143+
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath :
144+
locationPath + "/");
145+
if (!resourcePath.startsWith(locationPath)) {
146+
return false;
147+
}
148+
149+
if (resourcePath.contains("%")) {
150+
if (UriUtils.decode(resourcePath, "UTF-8").contains("../")) {
151+
return false;
152+
}
153+
}
154+
155+
return true;
156+
}
157+
158+
159+
}

0 commit comments

Comments
 (0)