1818
1919import java .io .IOException ;
2020import java .io .UncheckedIOException ;
21- import java .io .UnsupportedEncodingException ;
22- import java .net .URLDecoder ;
23- import java .nio .charset .StandardCharsets ;
2421import java .util .function .Function ;
2522
2623import reactor .core .publisher .Mono ;
2724
28- import org .springframework .core .io .ClassPathResource ;
2925import org .springframework .core .io .Resource ;
30- import org .springframework .core .io .UrlResource ;
3126import org .springframework .http .server .PathContainer ;
3227import org .springframework .util .Assert ;
33- import org .springframework .util .ResourceUtils ;
34- import org .springframework .util .StringUtils ;
35- import org .springframework .web .util .UriUtils ;
28+ import org .springframework .web .reactive .resource .ResourceHandlerUtils ;
3629import org .springframework .web .util .pattern .PathPattern ;
3730import org .springframework .web .util .pattern .PathPatternParser ;
3831
@@ -51,12 +44,11 @@ class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resourc
5144
5245 public PathResourceLookupFunction (String pattern , Resource location ) {
5346 Assert .hasLength (pattern , "'pattern' must not be empty" );
54- Assert . notNull (location , "'location' must not be null" );
47+ ResourceHandlerUtils . assertResourceLocation (location );
5548 this .pattern = PathPatternParser .defaultInstance .parse (pattern );
5649 this .location = location ;
5750 }
5851
59-
6052 @ Override
6153 public Mono <Resource > apply (ServerRequest request ) {
6254 PathContainer pathContainer = request .requestPath ().pathWithinApplication ();
@@ -65,21 +57,14 @@ public Mono<Resource> apply(ServerRequest request) {
6557 }
6658
6759 pathContainer = this .pattern .extractPathWithinPattern (pathContainer );
68- String path = processPath (pathContainer .value ());
69- if (!StringUtils .hasText (path ) || isInvalidPath (path )) {
70- return Mono .empty ();
71- }
72- if (isInvalidEncodedInputPath (path )) {
60+ String path = ResourceHandlerUtils .normalizeInputPath (pathContainer .value ());
61+ if (ResourceHandlerUtils .shouldIgnoreInputPath (path )) {
7362 return Mono .empty ();
7463 }
7564
76- if (!(this .location instanceof UrlResource )) {
77- path = UriUtils .decode (path , StandardCharsets .UTF_8 );
78- }
79-
8065 try {
81- Resource resource = this .location . createRelative ( path );
82- if (resource .isReadable () && isResourceUnderLocation (resource )) {
66+ Resource resource = ResourceHandlerUtils . createRelativeResource ( this .location , path );
67+ if (resource .isReadable () && ResourceHandlerUtils . isResourceUnderLocation (this . location , resource )) {
8368 return Mono .just (resource );
8469 }
8570 else {
@@ -91,150 +76,6 @@ public Mono<Resource> apply(ServerRequest request) {
9176 }
9277 }
9378
94- /**
95- * Process the given resource path.
96- * <p>The default implementation replaces:
97- * <ul>
98- * <li>Backslash with forward slash.
99- * <li>Duplicate occurrences of slash with a single slash.
100- * <li>Any combination of leading slash and control characters (00-1F and 7F)
101- * with a single "/" or "". For example {@code " / // foo/bar"}
102- * becomes {@code "/foo/bar"}.
103- * </ul>
104- */
105- protected String processPath (String path ) {
106- path = StringUtils .replace (path , "\\ " , "/" );
107- path = cleanDuplicateSlashes (path );
108- return cleanLeadingSlash (path );
109- }
110-
111- private String cleanDuplicateSlashes (String path ) {
112- StringBuilder sb = null ;
113- char prev = 0 ;
114- for (int i = 0 ; i < path .length (); i ++) {
115- char curr = path .charAt (i );
116- try {
117- if (curr == '/' && prev == '/' ) {
118- if (sb == null ) {
119- sb = new StringBuilder (path .substring (0 , i ));
120- }
121- continue ;
122- }
123- if (sb != null ) {
124- sb .append (path .charAt (i ));
125- }
126- }
127- finally {
128- prev = curr ;
129- }
130- }
131- return (sb != null ? sb .toString () : path );
132- }
133-
134- private String cleanLeadingSlash (String path ) {
135- boolean slash = false ;
136- for (int i = 0 ; i < path .length (); i ++) {
137- if (path .charAt (i ) == '/' ) {
138- slash = true ;
139- }
140- else if (path .charAt (i ) > ' ' && path .charAt (i ) != 127 ) {
141- if (i == 0 || (i == 1 && slash )) {
142- return path ;
143- }
144- return (slash ? "/" + path .substring (i ) : path .substring (i ));
145- }
146- }
147- return (slash ? "/" : "" );
148- }
149-
150- private boolean isInvalidPath (String path ) {
151- if (path .contains ("WEB-INF" ) || path .contains ("META-INF" )) {
152- return true ;
153- }
154- if (path .contains (":/" )) {
155- String relativePath = (path .charAt (0 ) == '/' ? path .substring (1 ) : path );
156- if (ResourceUtils .isUrl (relativePath ) || relativePath .startsWith ("url:" )) {
157- return true ;
158- }
159- }
160- return path .contains (".." ) && StringUtils .cleanPath (path ).contains ("../" );
161- }
162-
163- /**
164- * Check whether the given path contains invalid escape sequences.
165- * @param path the path to validate
166- * @return {@code true} if the path is invalid, {@code false} otherwise
167- */
168- private boolean isInvalidEncodedInputPath (String path ) {
169- if (path .contains ("%" )) {
170- try {
171- // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
172- String decodedPath = URLDecoder .decode (path , "UTF-8" );
173- if (isInvalidPath (decodedPath )) {
174- return true ;
175- }
176- decodedPath = processPath (decodedPath );
177- if (isInvalidPath (decodedPath )) {
178- return true ;
179- }
180- }
181- catch (IllegalArgumentException ex ) {
182- // May not be possible to decode...
183- }
184- catch (UnsupportedEncodingException ex ) {
185- throw new RuntimeException (ex );
186- }
187- }
188- return false ;
189- }
190-
191- private boolean isResourceUnderLocation (Resource resource ) throws IOException {
192- if (resource .getClass () != this .location .getClass ()) {
193- return false ;
194- }
195-
196- String resourcePath ;
197- String locationPath ;
198-
199- if (resource instanceof UrlResource ) {
200- resourcePath = resource .getURL ().toExternalForm ();
201- locationPath = StringUtils .cleanPath (this .location .getURL ().toString ());
202- }
203- else if (resource instanceof ClassPathResource ) {
204- resourcePath = ((ClassPathResource ) resource ).getPath ();
205- locationPath = StringUtils .cleanPath (((ClassPathResource ) this .location ).getPath ());
206- }
207- else {
208- resourcePath = resource .getURL ().getPath ();
209- locationPath = StringUtils .cleanPath (this .location .getURL ().getPath ());
210- }
211-
212- if (locationPath .equals (resourcePath )) {
213- return true ;
214- }
215- locationPath = (locationPath .endsWith ("/" ) || locationPath .isEmpty () ? locationPath : locationPath + "/" );
216- return (resourcePath .startsWith (locationPath ) && !isInvalidEncodedInputPath (resourcePath ));
217- }
218-
219- private boolean isInvalidEncodedResourcePath (String resourcePath ) {
220- if (resourcePath .contains ("%" )) {
221- // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
222- try {
223- String decodedPath = URLDecoder .decode (resourcePath , "UTF-8" );
224- if (decodedPath .contains ("../" ) || decodedPath .contains ("..\\ " )) {
225- return true ;
226- }
227- }
228- catch (IllegalArgumentException ex ) {
229- // May not be possible to decode...
230- }
231- catch (UnsupportedEncodingException ex ) {
232- throw new RuntimeException (ex );
233- }
234- }
235- return false ;
236- }
237-
23879 @ Override
23980 public String toString () {
24081 return this .pattern + " -> " + this .location ;
0 commit comments