Skip to content

Commit 43b507e

Browse files
committed
Fix inconsistent resource metadata with current GET and PUT/DELETE
Concurrent reads and writes (e.g. HTTP GET and PUT / DELETE) for the same path cause corruption of the FileResource where some of the fields are set as if the file exists and some as set as if it does not.
1 parent 5f0d580 commit 43b507e

File tree

4 files changed

+273
-29
lines changed

4 files changed

+273
-29
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.catalina;
18+
19+
import java.util.concurrent.atomic.AtomicInteger;
20+
import java.util.concurrent.locks.ReentrantReadWriteLock;
21+
22+
/**
23+
* Interface implemented by {@link WebResourceSet} implementations that wish to provide locking functionality.
24+
*/
25+
public interface WebResourceLockSet {
26+
27+
/**
28+
* Lock the resource at the provided path for reading. The resource is not required to exist. Read locks are not
29+
* exclusive.
30+
*
31+
* @param path The path to the resource to be locked for reading
32+
*
33+
* @return The {@link ResourceLock} that must be passed to {@link #unlockForRead(ResourceLock)} to release the lock
34+
*/
35+
ResourceLock lockForRead(String path);
36+
37+
/**
38+
* Release a read lock from the resource associated with the given {@link ResourceLock}.
39+
*
40+
* @param resourceLock The {@link ResourceLock} associated with the resource for which a read lock should be
41+
* released
42+
*/
43+
void unlockForRead(ResourceLock resourceLock);
44+
45+
/**
46+
* Lock the resource at the provided path for writing. The resource is not required to exist. Write locks are
47+
* exclusive.
48+
*
49+
* @param path The path to the resource to be locked for writing
50+
*
51+
* @return The {@link ResourceLock} that must be passed to {@link #unlockForWrite(ResourceLock)} to release the lock
52+
*/
53+
ResourceLock lockForWrite(String path);
54+
55+
/**
56+
* Release the write lock from the resource associated with the given {@link ResourceLock}.
57+
*
58+
* @param resourceLock The {@link ResourceLock} associated with the resource for which the write lock should be
59+
* released
60+
*/
61+
void unlockForWrite(ResourceLock resourceLock);
62+
63+
64+
class ResourceLock {
65+
public final AtomicInteger count = new AtomicInteger(0);
66+
public final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
67+
public final String key;
68+
69+
public ResourceLock(String key) {
70+
this.key = key;
71+
}
72+
}
73+
}

java/org/apache/catalina/webresources/DirResourceSet.java

Lines changed: 171 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,35 @@
2222
import java.io.InputStream;
2323
import java.nio.file.Files;
2424
import java.nio.file.StandardCopyOption;
25+
import java.util.HashMap;
26+
import java.util.Locale;
27+
import java.util.Map;
2528
import java.util.Set;
2629
import java.util.jar.Manifest;
2730

2831
import org.apache.catalina.LifecycleException;
2932
import org.apache.catalina.WebResource;
33+
import org.apache.catalina.WebResourceLockSet;
3034
import org.apache.catalina.WebResourceRoot;
3135
import org.apache.catalina.WebResourceRoot.ResourceSetType;
3236
import org.apache.catalina.util.ResourceSet;
3337
import org.apache.juli.logging.Log;
3438
import org.apache.juli.logging.LogFactory;
39+
import org.apache.tomcat.util.http.RequestUtil;
3540

3641
/**
3742
* Represents a {@link org.apache.catalina.WebResourceSet} based on a directory.
3843
*/
39-
public class DirResourceSet extends AbstractFileResourceSet {
44+
public class DirResourceSet extends AbstractFileResourceSet implements WebResourceLockSet {
4045

4146
private static final Log log = LogFactory.getLog(DirResourceSet.class);
4247

48+
private boolean caseSensitive = true;
49+
50+
private Map<String,ResourceLock> resourceLocksByPath = new HashMap<>();
51+
private Object resourceLocksByPathLock = new Object();
52+
53+
4354
/**
4455
* A no argument constructor is required for this to work with the digester.
4556
*/
@@ -91,22 +102,33 @@ public WebResource getResource(String path) {
91102
String webAppMount = getWebAppMount();
92103
WebResourceRoot root = getRoot();
93104
if (path.startsWith(webAppMount)) {
94-
File f = file(path.substring(webAppMount.length()), false);
95-
if (f == null) {
96-
return new EmptyResource(root, path);
97-
}
98-
if (!f.exists()) {
99-
return new EmptyResource(root, path, f);
100-
}
101-
if (f.isDirectory() && path.charAt(path.length() - 1) != '/') {
102-
path = path + '/';
105+
/*
106+
* Lock the path for reading until the WebResource has been constructed. The lock prevents concurrent reads
107+
* and writes (e.g. HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource
108+
* where some of the fields are set as if the file exists and some as set as if it does not.
109+
*/
110+
ResourceLock lock = lockForRead(path);
111+
try {
112+
File f = file(path.substring(webAppMount.length()), false);
113+
if (f == null) {
114+
return new EmptyResource(root, path);
115+
}
116+
if (!f.exists()) {
117+
return new EmptyResource(root, path, f);
118+
}
119+
if (f.isDirectory() && path.charAt(path.length() - 1) != '/') {
120+
path = path + '/';
121+
}
122+
return new FileResource(root, path, f, isReadOnly(), getManifest(), this, lock.key);
123+
} finally {
124+
unlockForRead(lock);
103125
}
104-
return new FileResource(root, path, f, isReadOnly(), getManifest());
105126
} else {
106127
return new EmptyResource(root, path);
107128
}
108129
}
109130

131+
110132
@Override
111133
public String[] list(String path) {
112134
checkPath(path);
@@ -246,32 +268,42 @@ public boolean write(String path, InputStream is, boolean overwrite) {
246268
return false;
247269
}
248270

249-
File dest = null;
250271
String webAppMount = getWebAppMount();
251-
if (path.startsWith(webAppMount)) {
272+
if (!path.startsWith(webAppMount)) {
273+
return false;
274+
}
275+
276+
File dest = null;
277+
/*
278+
* Lock the path for writing until the write is complete. The lock prevents concurrent reads and writes (e.g.
279+
* HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields
280+
* are set as if the file exists and some as set as if it does not.
281+
*/
282+
ResourceLock lock = lockForWrite(path);
283+
try {
252284
dest = file(path.substring(webAppMount.length()), false);
253285
if (dest == null) {
254286
return false;
255287
}
256-
} else {
257-
return false;
258-
}
259288

260-
if (dest.exists() && !overwrite) {
261-
return false;
262-
}
289+
if (dest.exists() && !overwrite) {
290+
return false;
291+
}
263292

264-
try {
265-
if (overwrite) {
266-
Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
267-
} else {
268-
Files.copy(is, dest.toPath());
293+
try {
294+
if (overwrite) {
295+
Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
296+
} else {
297+
Files.copy(is, dest.toPath());
298+
}
299+
} catch (IOException ioe) {
300+
return false;
269301
}
270-
} catch (IOException ioe) {
271-
return false;
272-
}
273302

274-
return true;
303+
return true;
304+
} finally {
305+
unlockForWrite(lock);
306+
}
275307
}
276308

277309
@Override
@@ -286,6 +318,7 @@ protected void checkType(File file) {
286318
@Override
287319
protected void initInternal() throws LifecycleException {
288320
super.initInternal();
321+
caseSensitive = isCaseSensitive();
289322
// Is this an exploded web application?
290323
if (getWebAppMount().equals("")) {
291324
// Look for a manifest
@@ -299,4 +332,114 @@ protected void initInternal() throws LifecycleException {
299332
}
300333
}
301334
}
335+
336+
337+
/*
338+
* Determines if this ResourceSet is based on a case sensitive file system or not.
339+
*/
340+
private boolean isCaseSensitive() {
341+
try {
342+
String canonicalPath = getFileBase().getCanonicalPath();
343+
File upper = new File(canonicalPath.toUpperCase(Locale.ENGLISH));
344+
if (!canonicalPath.equals(upper.getCanonicalPath())) {
345+
return true;
346+
}
347+
File lower = new File(canonicalPath.toLowerCase(Locale.ENGLISH));
348+
if (!canonicalPath.equals(lower.getCanonicalPath())) {
349+
return true;
350+
}
351+
/*
352+
* Both upper and lower case versions of the current fileBase have the same canonical path so the file
353+
* system must be case insensitive.
354+
*/
355+
} catch (IOException ioe) {
356+
log.warn(sm.getString("dirResourceSet.isCaseSensitive.fail", getFileBase().getAbsolutePath()), ioe);
357+
}
358+
359+
return false;
360+
}
361+
362+
363+
private String getLockKey(String path) {
364+
// Normalize path to ensure that the same key is used for the same path.
365+
String normalisedPath = RequestUtil.normalize(path);
366+
if (caseSensitive) {
367+
return normalisedPath;
368+
}
369+
return normalisedPath.toLowerCase(Locale.ENGLISH);
370+
}
371+
372+
373+
@Override
374+
public ResourceLock lockForRead(String path) {
375+
String key = getLockKey(path);
376+
ResourceLock resourceLock = null;
377+
synchronized (resourceLocksByPathLock) {
378+
/*
379+
* Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has
380+
* a consistent view of the currently "in-use" ResourceLocks.
381+
*/
382+
resourceLock = resourceLocksByPath.get(key);
383+
if (resourceLock == null) {
384+
resourceLock = new ResourceLock(key);
385+
}
386+
resourceLock.count.incrementAndGet();
387+
}
388+
// Obtain the lock outside the sync as it will block if there is a current write lock.
389+
resourceLock.reentrantLock.readLock().lock();
390+
return resourceLock;
391+
}
392+
393+
394+
@Override
395+
public void unlockForRead(ResourceLock resourceLock) {
396+
// Unlock outside the sync as there is no need to do it inside.
397+
resourceLock.reentrantLock.readLock().unlock();
398+
synchronized (resourceLocksByPathLock) {
399+
/*
400+
* Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that
401+
* map always has a consistent view of the currently "in-use" ResourceLocks.
402+
*/
403+
if (resourceLock.count.decrementAndGet() == 0) {
404+
resourceLocksByPath.remove(resourceLock.key);
405+
}
406+
}
407+
}
408+
409+
410+
@Override
411+
public ResourceLock lockForWrite(String path) {
412+
String key = getLockKey(path);
413+
ResourceLock resourceLock = null;
414+
synchronized (resourceLocksByPathLock) {
415+
/*
416+
* Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has
417+
* a consistent view of the currently "in-use" ResourceLocks.
418+
*/
419+
resourceLock = resourceLocksByPath.get(key);
420+
if (resourceLock == null) {
421+
resourceLock = new ResourceLock(key);
422+
}
423+
resourceLock.count.incrementAndGet();
424+
}
425+
// Obtain the lock outside the sync as it will block if there are any other current locks.
426+
resourceLock.reentrantLock.writeLock().lock();
427+
return resourceLock;
428+
}
429+
430+
431+
@Override
432+
public void unlockForWrite(ResourceLock resourceLock) {
433+
// Unlock outside the sync as there is no need to do it inside.
434+
resourceLock.reentrantLock.writeLock().unlock();
435+
synchronized (resourceLocksByPathLock) {
436+
/*
437+
* Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that
438+
* map always has a consistent view of the currently "in-use" ResourceLocks.
439+
*/
440+
if (resourceLock.count.decrementAndGet() == 0) {
441+
resourceLocksByPath.remove(resourceLock.key);
442+
}
443+
}
444+
}
302445
}

java/org/apache/catalina/webresources/FileResource.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.security.cert.Certificate;
3232
import java.util.jar.Manifest;
3333

34+
import org.apache.catalina.WebResourceLockSet;
35+
import org.apache.catalina.WebResourceLockSet.ResourceLock;
3436
import org.apache.catalina.WebResourceRoot;
3537
import org.apache.juli.logging.Log;
3638
import org.apache.juli.logging.LogFactory;
@@ -62,10 +64,20 @@ public class FileResource extends AbstractResource {
6264
private final boolean readOnly;
6365
private final Manifest manifest;
6466
private final boolean needConvert;
67+
private final WebResourceLockSet lockSet;
68+
private final String lockKey;
6569

6670
public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest) {
71+
this(root, webAppPath, resource, readOnly, manifest, null, null);
72+
}
73+
74+
75+
public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest,
76+
WebResourceLockSet lockSet, String lockKey) {
6777
super(root, webAppPath);
6878
this.resource = resource;
79+
this.lockSet = lockSet;
80+
this.lockKey = lockKey;
6981

7082
if (webAppPath.charAt(webAppPath.length() - 1) == '/') {
7183
String realName = resource.getName() + '/';
@@ -117,7 +129,22 @@ public boolean delete() {
117129
if (readOnly) {
118130
return false;
119131
}
120-
return resource.delete();
132+
/*
133+
* Lock the path for writing until the delete is complete. The lock prevents concurrent reads and writes (e.g.
134+
* HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields
135+
* are set as if the file exists and some as set as if it does not.
136+
*/
137+
ResourceLock lock = null;
138+
if (lockSet != null) {
139+
lock = lockSet.lockForWrite(lockKey);
140+
}
141+
try {
142+
return resource.delete();
143+
} finally {
144+
if (lockSet != null) {
145+
lockSet.unlockForWrite(lock);
146+
}
147+
}
121148
}
122149

123150
@Override

java/org/apache/catalina/webresources/LocalStrings.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ cachedResource.invalidURL=Unable to create an instance of CachedResourceURLStrea
3636

3737
classpathUrlStreamHandler.notFound=Unable to load the resource [{0}] using the thread context class loader or the current class''s class loader
3838

39+
dirResourceSet.isCaseSensitive.fail=Error trying to determine if file system at [{0}] is case sensitive so assuming it is not case sensitive
3940
dirResourceSet.manifestFail=Failed to read manifest from [{0}]
4041
dirResourceSet.notDirectory=The directory specified by base and internal path [{0}]{1}[{2}] does not exist.
4142
dirResourceSet.writeNpe=The input stream may not be null

0 commit comments

Comments
 (0)