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