1 package io.undertow.server.handlers.resource;
2
3
4 import io.undertow.UndertowLogger;
5 import io.undertow.UndertowMessages;
6 import io.undertow.util.ETag;
7 import org.jboss.logging.Logger;
8 import org.xnio.FileChangeCallback;
9 import org.xnio.FileChangeEvent;
10 import org.xnio.FileSystemWatcher;
11 import org.xnio.OptionMap;
12 import org.xnio.Xnio;
13
14 import java.io.File;
15 import java.io.IOException;
16 import java.nio.file.FileSystem;
17 import java.nio.file.FileSystems;
18 import java.nio.file.Files;
19 import java.nio.file.Path;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.List;
24 import java.util.TreeSet;
25
26
29 public class PathResourceManager implements ResourceManager {
30
31 private static final Logger log = Logger.getLogger(PathResourceManager.class.getName());
32
33 private static final boolean DEFAULT_CHANGE_LISTENERS_ALLOWED = !Boolean.getBoolean("io.undertow.disable-file-system-watcher");
34 private static final long DEFAULT_TRANSFER_MIN_SIZE = 1024;
35 private static final ETagFunction NULL_ETAG_FUNCTION = new ETagFunction() {
36 @Override
37 public ETag generate(Path path) {
38 return null;
39 }
40 };
41
42 private final List<ResourceChangeListener> listeners = new ArrayList<>();
43
44 private FileSystemWatcher fileSystemWatcher;
45
46 protected volatile String base;
47
48 protected volatile FileSystem fileSystem;
49
50
53 private final long transferMinSize;
54
55
59 private final boolean caseSensitive;
60
61
64 private final boolean followLinks;
65
66
70 private final TreeSet<String> safePaths = new TreeSet<>();
71
72 private final ETagFunction eTagFunction;
73
74 private final boolean allowResourceChangeListeners;
75
76 public PathResourceManager(final Path base) {
77 this(base, DEFAULT_TRANSFER_MIN_SIZE, true, false, null);
78 }
79
80 public PathResourceManager(final Path base, long transferMinSize) {
81 this(base, transferMinSize, true, false, null);
82 }
83
84 public PathResourceManager(final Path base, long transferMinSize, boolean caseSensitive) {
85 this(base, transferMinSize, caseSensitive, false, null);
86 }
87
88 public PathResourceManager(final Path base, long transferMinSize, boolean followLinks, final String... safePaths) {
89 this(base, transferMinSize, true, followLinks, safePaths);
90 }
91
92 protected PathResourceManager(long transferMinSize, boolean caseSensitive, boolean followLinks, final String... safePaths) {
93 this(transferMinSize, caseSensitive, followLinks, DEFAULT_CHANGE_LISTENERS_ALLOWED, safePaths);
94 }
95
96 protected PathResourceManager(long transferMinSize, boolean caseSensitive, boolean followLinks, boolean allowResourceChangeListeners, final String... safePaths) {
97 this.fileSystem = FileSystems.getDefault();
98 this.caseSensitive = caseSensitive;
99 this.followLinks = followLinks;
100 this.transferMinSize = transferMinSize;
101 this.allowResourceChangeListeners = allowResourceChangeListeners;
102 if (this.followLinks) {
103 if (safePaths == null) {
104 throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
105 }
106 for (final String safePath : safePaths) {
107 if (safePath == null) {
108 throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
109 }
110 }
111 this.safePaths.addAll(Arrays.asList(safePaths));
112 }
113 this.eTagFunction = NULL_ETAG_FUNCTION;
114 }
115
116 public PathResourceManager(final Path base, long transferMinSize, boolean caseSensitive, boolean followLinks, final String... safePaths) {
117 this(base, transferMinSize, caseSensitive, followLinks, DEFAULT_CHANGE_LISTENERS_ALLOWED, safePaths);
118 }
119
120 public PathResourceManager(final Path base, long transferMinSize, boolean caseSensitive, boolean followLinks, boolean allowResourceChangeListeners, final String... safePaths) {
121 this(builder()
122 .setBase(base)
123 .setTransferMinSize(transferMinSize)
124 .setCaseSensitive(caseSensitive)
125 .setFollowLinks(followLinks)
126 .setAllowResourceChangeListeners(allowResourceChangeListeners)
127 .setSafePaths(safePaths));
128 }
129
130 private PathResourceManager(Builder builder) {
131 this.allowResourceChangeListeners = builder.allowResourceChangeListeners;
132 if (builder.base == null) {
133 throw UndertowMessages.MESSAGES.argumentCannotBeNull("base");
134 }
135 this.fileSystem = builder.base.getFileSystem();
136 String basePath = builder.base.normalize().toAbsolutePath().toString();
137 if (!basePath.endsWith(fileSystem.getSeparator())) {
138 basePath = basePath + fileSystem.getSeparator();
139 }
140 this.base = basePath;
141 this.transferMinSize = builder.transferMinSize;
142 this.caseSensitive = builder.caseSensitive;
143 this.followLinks = builder.followLinks;
144 if (this.followLinks) {
145 if (builder.safePaths == null) {
146 throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
147 }
148 for (final String safePath : builder.safePaths) {
149 if (safePath == null) {
150 throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
151 }
152 }
153 this.safePaths.addAll(Arrays.asList(builder.safePaths));
154 }
155 this.eTagFunction = builder.eTagFunction;
156 }
157
158 public Path getBasePath() {
159 return fileSystem.getPath(base);
160 }
161
162 public PathResourceManager setBase(final Path base) {
163 if (base == null) {
164 throw UndertowMessages.MESSAGES.argumentCannotBeNull("base");
165 }
166 this.fileSystem = base.getFileSystem();
167 String basePath = base.toAbsolutePath().toString();
168 if (!basePath.endsWith(fileSystem.getSeparator())) {
169 basePath = basePath + fileSystem.getSeparator();
170 }
171 this.base = basePath;
172 return this;
173 }
174
175 public PathResourceManager setBase(final File base) {
176 if (base == null) {
177 throw UndertowMessages.MESSAGES.argumentCannotBeNull("base");
178 }
179 this.fileSystem = FileSystems.getDefault();
180 String basePath = base.getAbsolutePath();
181 if (!basePath.endsWith(fileSystem.getSeparator())) {
182 basePath = basePath + fileSystem.getSeparator();
183 }
184 this.base = basePath;
185 return this;
186 }
187
188 public Resource getResource(final String p) {
189 String path;
190
191 if (p.startsWith("/")) {
192 path = p.substring(1);
193 } else {
194 path = p;
195 }
196 try {
197 Path file = fileSystem.getPath(base, path);
198 String normalizedFile = file.normalize().toString();
199 if(!normalizedFile.startsWith(base)) {
200 if(normalizedFile.length() == base.length() - 1) {
201
202 if(!base.startsWith(normalizedFile)) {
203 log.tracef("Failed to get path resource %s from path resource manager with base %s, as file was outside the base directory", p, base);
204 return null;
205 }
206 } else {
207 log.tracef("Failed to get path resource %s from path resource manager with base %s, as file was outside the base directory", p, base);
208 return null;
209 }
210 }
211 if (Files.exists(file)) {
212 if(path.endsWith("/") && ! Files.isDirectory(file)) {
213
214 log.tracef("Failed to get path resource %s from path resource manager with base %s, as path ended with a / but was not a directory", p, base);
215 return null;
216 }
217 boolean followAll = this.followLinks && safePaths.isEmpty();
218 SymlinkResult symlinkBase = getSymlinkBase(base, file);
219 if (!followAll && symlinkBase != null && symlinkBase.requiresCheck) {
220 if (this.followLinks && isSymlinkSafe(file)) {
221 return getFileResource(file, path, symlinkBase.path, normalizedFile);
222 } else {
223 log.tracef("Failed to get path resource %s from path resource manager with base %s, as it was not a safe symlink path", p, base);
224 return null;
225 }
226 } else {
227 return getFileResource(file, path, symlinkBase == null ? null : symlinkBase.path, normalizedFile);
228 }
229 } else {
230 log.tracef("Failed to get path resource %s from path resource manager with base %s, as the path did not exist", p, base);
231 return null;
232 }
233 } catch (Exception e) {
234 UndertowLogger.REQUEST_LOGGER.debugf(e, "Invalid path %s", p);
235 return null;
236 }
237 }
238
239 @Override
240 public boolean isResourceChangeListenerSupported() {
241 return allowResourceChangeListeners;
242 }
243
244 @Override
245 public synchronized void registerResourceChangeListener(ResourceChangeListener listener) {
246 if(!allowResourceChangeListeners) {
247
248
249 return;
250 }
251 if (!fileSystem.equals(FileSystems.getDefault())) {
252 throw new IllegalStateException("Resource change listeners not supported when using a non-default file system");
253 }
254 listeners.add(listener);
255 if (fileSystemWatcher == null) {
256 fileSystemWatcher = Xnio.getInstance().createFileSystemWatcher("Watcher for " + base, OptionMap.EMPTY);
257 fileSystemWatcher.watchPath(new File(base), new FileChangeCallback() {
258 @Override
259 public void handleChanges(Collection<FileChangeEvent> changes) {
260 synchronized (PathResourceManager.this) {
261 final List<ResourceChangeEvent> events = new ArrayList<>();
262 for (FileChangeEvent change : changes) {
263 if (change.getFile().getAbsolutePath().startsWith(base)) {
264 String path = change.getFile().getAbsolutePath().substring(base.length());
265 if (File.separatorChar == '\\' && path.contains(File.separator)) {
266 path = path.replace(File.separatorChar, '/');
267 }
268 events.add(new ResourceChangeEvent(path, ResourceChangeEvent.Type.valueOf(change.getType().name())));
269 }
270 }
271 for (ResourceChangeListener listener : listeners) {
272 listener.handleChanges(events);
273 }
274 }
275 }
276 });
277 }
278 }
279
280
281 @Override
282 public synchronized void removeResourceChangeListener(ResourceChangeListener listener) {
283 if(!allowResourceChangeListeners) {
284 return;
285 }
286 listeners.remove(listener);
287 }
288
289 public long getTransferMinSize() {
290 return transferMinSize;
291 }
292
293 @Override
294 public synchronized void close() throws IOException {
295 if (fileSystemWatcher != null) {
296 fileSystemWatcher.close();
297 }
298 }
299
300
303 private SymlinkResult getSymlinkBase(final String base, final Path file) throws IOException {
304 int nameCount = file.getNameCount();
305 Path root = fileSystem.getPath(base);
306 int rootCount = root.getNameCount();
307 Path f = file;
308 for (int i = nameCount - 1; i>=0; i--) {
309 if (SecurityActions.isSymbolicLink(f)) {
310 return new SymlinkResult(i+1 > rootCount, f);
311 }
312 f = f.getParent();
313 }
314
315 return null;
316 }
317
318
328 private boolean isFileSameCase(final Path file, String normalizeFile) throws IOException {
329 String canonicalName = file.toRealPath().toString();
330 return canonicalName.equals(normalizeFile);
331 }
332
333
337 private boolean isSymlinkSafe(final Path file) throws IOException {
338 String canonicalPath = file.toRealPath().toString();
339 for (String safePath : this.safePaths) {
340 if (safePath.length() > 0) {
341 if (safePath.startsWith(fileSystem.getSeparator())) {
342
345 if (safePath.length() > 0 &&
346 canonicalPath.length() >= safePath.length() &&
347 canonicalPath.startsWith(safePath)) {
348 return true;
349 }
350 } else {
351
354 String absSafePath = base + fileSystem.getSeparator() + safePath;
355 Path absSafePathFile = fileSystem.getPath(absSafePath);
356 String canonicalSafePath = absSafePathFile.toRealPath().toString();
357 if (canonicalSafePath.length() > 0 &&
358 canonicalPath.length() >= canonicalSafePath.length() &&
359 canonicalPath.startsWith(canonicalSafePath)) {
360 return true;
361 }
362
363 }
364 }
365 }
366 return false;
367 }
368
369
372 protected PathResource getFileResource(final Path file, final String path, final Path symlinkBase, String normalizedFile) throws IOException {
373 if (this.caseSensitive) {
374 if (symlinkBase != null) {
375 String relative = symlinkBase.relativize(file.normalize()).toString();
376 String fileResolved = file.toRealPath().toString();
377 String symlinkBaseResolved = symlinkBase.toRealPath().toString();
378 if (!fileResolved.startsWith(symlinkBaseResolved)) {
379 log.tracef("Rejected path resource %s from path resource manager with base %s, as the case did not match actual case of %s", path, base, normalizedFile);
380 return null;
381 }
382 String compare = fileResolved.substring(symlinkBaseResolved.length());
383 if(compare.startsWith(fileSystem.getSeparator())) {
384 compare = compare.substring(fileSystem.getSeparator().length());
385 }
386 if(relative.startsWith(fileSystem.getSeparator())) {
387 relative = relative.substring(fileSystem.getSeparator().length());
388 }
389 if (relative.equals(compare)) {
390 log.tracef("Found path resource %s from path resource manager with base %s", path, base);
391 return new PathResource(file, this, path, eTagFunction.generate(file));
392 }
393 log.tracef("Rejected path resource %s from path resource manager with base %s, as the case did not match actual case of %s", path, base, normalizedFile);
394 return null;
395 } else if (isFileSameCase(file, normalizedFile)) {
396 log.tracef("Found path resource %s from path resource manager with base %s", path, base);
397 return new PathResource(file, this, path, eTagFunction.generate(file));
398 } else {
399 log.tracef("Rejected path resource %s from path resource manager with base %s, as the case did not match actual case of %s", path, base, normalizedFile);
400 return null;
401 }
402 } else {
403 log.tracef("Found path resource %s from path resource manager with base %s", path, base);
404 return new PathResource(file, this, path, eTagFunction.generate(file));
405 }
406 }
407
408 private static class SymlinkResult {
409 public final boolean requiresCheck;
410 public final Path path;
411
412 private SymlinkResult(boolean requiresCheck, Path path) {
413 this.requiresCheck = requiresCheck;
414 this.path = path;
415 }
416 }
417
418 public interface ETagFunction {
419
420
426 ETag generate(Path path);
427 }
428
429 public static Builder builder() {
430 return new Builder();
431 }
432
433 public static final class Builder {
434
435 private Path base;
436 private long transferMinSize = DEFAULT_TRANSFER_MIN_SIZE;
437 private boolean caseSensitive = true;
438 private boolean followLinks = false;
439 private boolean allowResourceChangeListeners = DEFAULT_CHANGE_LISTENERS_ALLOWED;
440 private ETagFunction eTagFunction = NULL_ETAG_FUNCTION;
441 private String[] safePaths;
442
443 private Builder() {
444 }
445
446 public Builder setBase(Path base) {
447 this.base = base;
448 return this;
449 }
450
451 public Builder setTransferMinSize(long transferMinSize) {
452 this.transferMinSize = transferMinSize;
453 return this;
454 }
455
456 public Builder setCaseSensitive(boolean caseSensitive) {
457 this.caseSensitive = caseSensitive;
458 return this;
459 }
460
461 public Builder setFollowLinks(boolean followLinks) {
462 this.followLinks = followLinks;
463 return this;
464 }
465
466 public Builder setAllowResourceChangeListeners(boolean allowResourceChangeListeners) {
467 this.allowResourceChangeListeners = allowResourceChangeListeners;
468 return this;
469 }
470
471 public Builder setETagFunction(ETagFunction eTagFunction) {
472 this.eTagFunction = eTagFunction;
473 return this;
474 }
475
476 public Builder setSafePaths(String[] safePaths) {
477 this.safePaths = safePaths;
478 return this;
479 }
480
481 public ResourceManager build() {
482 return new PathResourceManager(this);
483 }
484 }
485 }
486