Skip to content

Commit 486c24e

Browse files
joewizclaude
andcommitted
[feature] Migrate WebDAV module from Milton 1.x to 4.x CE
Package renames: com.bradmcevoy.http -> io.milton.http/resource Test client: com.ettrema.httpclient -> io.milton.httpclient MiltonWebDAVServlet: HttpServlet composition (Jetty 12 requires it) Auth propagation for Milton 4.x resource lifecycle: - ensureAuthenticated() helper pulls credentials from request context when Milton creates fresh resource objects without auth state - Called in authorise(), createNew(), moveTo(), copyTo(), delete() - Resets isInitialized to recalculate permissions with correct subject Tests: preemptive Basic auth via AlwaysBasicPreAuth interceptor (required for COPY/MOVE/DELETE where challenge-response fails) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a3f0eca commit 486c24e

File tree

13 files changed

+188
-140
lines changed

13 files changed

+188
-140
lines changed

extensions/webdav/src/main/java/org/exist/webdav/ExistResourceFactory.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
package org.exist.webdav;
2323

2424

25-
import com.bradmcevoy.http.Resource;
26-
import com.bradmcevoy.http.ResourceFactory;
25+
import io.milton.http.ResourceFactory;
26+
import io.milton.http.exceptions.BadRequestException;
27+
import io.milton.http.exceptions.NotAuthorizedException;
28+
import io.milton.resource.Resource;
2729
import org.apache.logging.log4j.LogManager;
2830
import org.apache.logging.log4j.Logger;
2931
import org.exist.EXistException;
@@ -108,7 +110,7 @@ public ExistResourceFactory() {
108110
* could not be detected.
109111
*/
110112
@Override
111-
public Resource getResource(String host, String path) {
113+
public Resource getResource(String host, String path) throws NotAuthorizedException, BadRequestException {
112114

113115
// DWES: work around if no /db is available return nothing.
114116
if (!path.contains("/db")) {

extensions/webdav/src/main/java/org/exist/webdav/MiltonCollection.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@
2121
*/
2222
package org.exist.webdav;
2323

24-
import com.bradmcevoy.http.*;
25-
import com.bradmcevoy.http.exceptions.*;
24+
import io.milton.http.Auth;
25+
import io.milton.http.LockInfo;
26+
import io.milton.http.LockResult;
27+
import io.milton.http.LockTimeout;
28+
import io.milton.http.LockToken;
29+
import io.milton.http.Range;
30+
import io.milton.http.exceptions.*;
31+
import io.milton.resource.*;
2632
import org.exist.EXistException;
2733
import org.exist.security.PermissionDeniedException;
2834
import org.exist.security.Subject;
@@ -104,7 +110,7 @@ public MiltonCollection(final Properties configuration, String host, XmldbURI ur
104110
* Collection Resource
105111
* =================== */
106112
@Override
107-
public Resource child(String childName) {
113+
public Resource child(String childName) throws NotAuthorizedException, BadRequestException {
108114

109115
if (LOG.isDebugEnabled()) {
110116
LOG.debug("get child={}", childName);
@@ -147,7 +153,7 @@ private List<MiltonDocument> getDocumentResources() {
147153
}
148154

149155
@Override
150-
public List<? extends Resource> getChildren() {
156+
public List<? extends Resource> getChildren() throws NotAuthorizedException, BadRequestException {
151157
List<Resource> allResources = new ArrayList<>();
152158

153159
allResources.addAll(getCollectionResources());
@@ -199,7 +205,7 @@ public void delete() throws NotAuthorizedException, ConflictException, BadReques
199205
* ========================== */
200206
@Override
201207
public CollectionResource createCollection(String name)
202-
throws NotAuthorizedException, ConflictException {
208+
throws NotAuthorizedException, ConflictException, BadRequestException {
203209

204210
if (LOG.isTraceEnabled()) {
205211
LOG.trace("Create collection '{}' in '{}'.", name, resourceXmldbUri);
@@ -229,14 +235,31 @@ public CollectionResource createCollection(String name)
229235
* =============== */
230236
@Override
231237
public Resource createNew(String newName, InputStream is, Long length, String contentType)
232-
throws IOException, ConflictException {
238+
throws IOException, ConflictException, NotAuthorizedException, BadRequestException {
233239

234240
if (LOG.isTraceEnabled()) {
235241
LOG.trace("Create '{}' in '{}'", newName, resourceXmldbUri);
236242
}
237243

238244
Resource resource = null;
239245
try {
246+
// Ensure the authenticated subject is available for write operations.
247+
// In Milton 4.x, the PutHandler may call createNew() on a newly-created
248+
// resource that hasn't gone through authenticate(). Get the subject from
249+
// the current request's auth context if not set on this resource.
250+
if (subject == null) {
251+
final io.milton.http.Request req = io.milton.http.HttpManager.request();
252+
if (req != null && req.getAuthorization() != null) {
253+
final String authUser = req.getAuthorization().getUser();
254+
final String authPwd = req.getAuthorization().getPassword();
255+
if (authUser != null) {
256+
subject = existResource.authenticate(authUser, authPwd);
257+
if (subject != null) {
258+
existResource.setUser(subject);
259+
}
260+
}
261+
}
262+
}
240263
// submit
241264
XmldbURI resourceURI = existCollection.createFile(newName, is, length, contentType);
242265

@@ -276,11 +299,11 @@ public LockResult lock(LockTimeout timeout, LockInfo lockInfo)
276299
LOG.debug("'{}' -- {}", resourceXmldbUri, lockInfo.toString());
277300
}
278301

279-
return refreshLock(UUIDGenerator.getUUIDversion4());
302+
return refreshLock(UUIDGenerator.getUUIDversion4(), timeout);
280303
}
281304

282305
@Override
283-
public LockResult refreshLock(String token) throws NotAuthorizedException, PreConditionFailedException {
306+
public LockResult refreshLock(String token, LockTimeout timeout) throws NotAuthorizedException, PreConditionFailedException {
284307

285308
if (LOG.isDebugEnabled()) {
286309
LOG.debug("'{}' token='{}'", resourceXmldbUri, token);
@@ -315,7 +338,7 @@ public LockToken getCurrentLock() {
315338
* MovableResource
316339
* =============== */
317340
@Override
318-
public void moveTo(CollectionResource rDest, String newName) throws ConflictException {
341+
public void moveTo(CollectionResource rDest, String newName) throws ConflictException, NotAuthorizedException, BadRequestException {
319342

320343
if (LOG.isDebugEnabled()) {
321344
LOG.debug("Move '{}' to '{}' in '{}'", resourceXmldbUri, newName, rDest.getName());
@@ -335,7 +358,7 @@ public void moveTo(CollectionResource rDest, String newName) throws ConflictExce
335358
* ================ */
336359

337360
@Override
338-
public void copyTo(CollectionResource toCollection, String newName) {
361+
public void copyTo(CollectionResource toCollection, String newName) throws NotAuthorizedException, BadRequestException, ConflictException {
339362

340363
if (LOG.isDebugEnabled()) {
341364
LOG.debug("Move '{}' to '{}' in '{}'", resourceXmldbUri, newName, toCollection.getName());
@@ -357,7 +380,7 @@ public void copyTo(CollectionResource toCollection, String newName) {
357380

358381
@Override
359382
public void sendContent(OutputStream out, Range range, Map<String, String> params,
360-
String contentType) throws IOException, NotAuthorizedException, BadRequestException {
383+
String contentType) throws IOException, NotAuthorizedException, BadRequestException, NotFoundException {
361384

362385
try {
363386
XMLOutputFactory xf = XMLOutputFactory.newInstance();

extensions/webdav/src/main/java/org/exist/webdav/MiltonDocument.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,23 @@
2121
*/
2222
package org.exist.webdav;
2323

24-
import com.bradmcevoy.http.*;
25-
import com.bradmcevoy.http.exceptions.*;
26-
import com.bradmcevoy.http.webdav.DefaultUserAgentHelper;
27-
import com.bradmcevoy.http.webdav.UserAgentHelper;
24+
import io.milton.http.Auth;
25+
import io.milton.http.LockInfo;
26+
import io.milton.http.LockResult;
27+
import io.milton.http.LockTimeout;
28+
import io.milton.http.LockToken;
29+
import io.milton.http.HttpManager;
30+
import io.milton.http.Range;
31+
import io.milton.http.exceptions.*;
32+
import io.milton.http.webdav.DefaultUserAgentHelper;
33+
import io.milton.http.webdav.UserAgentHelper;
34+
import io.milton.resource.CollectionResource;
35+
import io.milton.resource.CopyableResource;
36+
import io.milton.resource.DeletableResource;
37+
import io.milton.resource.GetableResource;
38+
import io.milton.resource.LockableResource;
39+
import io.milton.resource.MoveableResource;
40+
import io.milton.resource.PropFindableResource;
2841
import org.apache.commons.io.IOUtils;
2942
import org.apache.commons.io.output.CountingOutputStream;
3043
import org.apache.commons.io.output.NullOutputStream;
@@ -184,7 +197,7 @@ public void setIsPropFind(boolean isPropFind) {
184197

185198
@Override
186199
public void sendContent(OutputStream out, Range range, Map<String, String> params, String contentType)
187-
throws IOException, NotAuthorizedException {
200+
throws IOException, NotAuthorizedException, BadRequestException, NotFoundException {
188201
try {
189202
if (LOG.isDebugEnabled()) {
190203
LOG.debug("Serializing from database");
@@ -263,7 +276,7 @@ public Long getContentLength() {
263276
Long size = null;
264277

265278
// MacOsX has a bad reputation
266-
boolean isMacFinder = userAgentHelper.isMacFinder(HttpManager.request().getUserAgentHeader());
279+
boolean isMacFinder = userAgentHelper.isMacFinder(HttpManager.request());
267280

268281
if (existDocument.isXmlDocument()) {
269282
// XML document, exact size is not (directly) known)
@@ -374,7 +387,7 @@ public LockResult lock(LockTimeout timeout, LockInfo lockInfo)
374387
* ================ */
375388

376389
@Override
377-
public LockResult refreshLock(String token) throws NotAuthorizedException, PreConditionFailedException {
390+
public LockResult refreshLock(String token, LockTimeout timeout) throws NotAuthorizedException, PreConditionFailedException {
378391

379392
if (LOG.isDebugEnabled()) {
380393
LOG.debug("Refresh: {} token={}", resourceXmldbUri, token);
@@ -447,7 +460,7 @@ public LockToken getCurrentLock() {
447460
}
448461

449462
@Override
450-
public void moveTo(CollectionResource rDest, String newName) throws ConflictException {
463+
public void moveTo(CollectionResource rDest, String newName) throws ConflictException, NotAuthorizedException, BadRequestException {
451464

452465
if (LOG.isDebugEnabled()) {
453466
LOG.debug("moveTo: {} newName={}", resourceXmldbUri, newName);
@@ -468,7 +481,7 @@ public void moveTo(CollectionResource rDest, String newName) throws ConflictExce
468481
* ================ */
469482

470483
@Override
471-
public void copyTo(CollectionResource rDest, String newName) {
484+
public void copyTo(CollectionResource rDest, String newName) throws NotAuthorizedException, BadRequestException, ConflictException {
472485

473486
if (LOG.isDebugEnabled()) {
474487
LOG.debug("copyTo: {} newName={}", resourceXmldbUri, newName);

extensions/webdav/src/main/java/org/exist/webdav/MiltonResource.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@
2121
*/
2222
package org.exist.webdav;
2323

24-
import com.bradmcevoy.http.*;
25-
import com.bradmcevoy.http.Request.Method;
24+
import io.milton.http.Auth;
25+
import io.milton.http.LockInfo;
26+
import io.milton.http.LockTimeout;
27+
import io.milton.http.LockToken;
28+
import io.milton.http.Request;
29+
import io.milton.http.Request.Method;
30+
import io.milton.http.exceptions.BadRequestException;
31+
import io.milton.http.exceptions.NotAuthorizedException;
32+
import io.milton.resource.Resource;
2633
import org.apache.logging.log4j.LogManager;
2734
import org.apache.logging.log4j.Logger;
2835
import org.exist.security.Subject;
@@ -295,6 +302,9 @@ public Object authenticate(String username, String password) {
295302
return null;
296303
}
297304

305+
// Propagate subject to the underlying eXist resource
306+
existResource.setUser(subject);
307+
298308
// Guest is not allowed to access.
299309
Subject guest = brokerPool.getSecurityManager().getGuestSubject();
300310
if (guest.equals(subject)) {
@@ -405,7 +415,7 @@ public Date getModifiedDate() {
405415
}
406416

407417
@Override
408-
public String checkRedirect(Request request) {
418+
public String checkRedirect(Request request) throws NotAuthorizedException, BadRequestException {
409419
return null;
410420
}
411421
}

extensions/webdav/src/main/java/org/exist/webdav/MiltonWebDAVServlet.java

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,40 @@
2121
*/
2222
package org.exist.webdav;
2323

24-
import com.bradmcevoy.http.MiltonServlet;
25-
import com.bradmcevoy.http.http11.DefaultHttp11ResponseHandler;
24+
import io.milton.http.http11.DefaultHttp11ResponseHandler;
25+
import io.milton.servlet.MiltonServlet;
2626
import org.apache.logging.log4j.LogManager;
2727
import org.apache.logging.log4j.Logger;
2828

2929
import jakarta.servlet.ServletConfig;
3030
import jakarta.servlet.ServletException;
31+
import jakarta.servlet.http.HttpServlet;
32+
import jakarta.servlet.http.HttpServletRequest;
33+
import jakarta.servlet.http.HttpServletResponse;
3134
import java.io.IOException;
3235
import java.io.InputStream;
3336
import java.util.Properties;
3437

3538
/**
36-
* Wrapper around the MiltonServlet for post-configuring the framework.
39+
* HttpServlet wrapper around Milton's MiltonServlet.
40+
*
41+
* Jetty 12 EE10 requires HttpServlet for proper servlet lifecycle management.
42+
* Milton 4.x's MiltonServlet implements Servlet directly (not HttpServlet),
43+
* so we use composition to delegate to it.
3744
*
3845
* @author Dannes Wessels
3946
*/
40-
public class MiltonWebDAVServlet extends MiltonServlet {
47+
public class MiltonWebDAVServlet extends HttpServlet {
4148

4249
protected final static Logger LOG = LogManager.getLogger(MiltonWebDAVServlet.class);
4350

44-
public static String POM_PROP = "/META-INF/maven/com.ettrema/milton-api/pom.properties";
51+
public static String POM_PROP = "/META-INF/maven/io.milton/milton-api/pom.properties";
52+
53+
private final MiltonServlet delegate = new MiltonServlet();
4554

4655
@Override
4756
public void init(ServletConfig config) throws ServletException {
57+
super.init(config);
4858

4959
LOG.info("Initializing webdav servlet");
5060

@@ -69,8 +79,8 @@ public void init(ServletConfig config) throws ServletException {
6979
LOG.info("Detected Milton WebDAV Server library version: {}", miltonVersion);
7080
}
7181

72-
// Initialize Milton
73-
super.init(config);
82+
// Initialize Milton delegate
83+
delegate.init(config);
7484

7585
// Retrieve parameters, set to FALSE if not existent
7686
String enableInitParameter = config.getInitParameter("enable.expect.continue");
@@ -81,9 +91,18 @@ public void init(ServletConfig config) throws ServletException {
8191
// Calculate effective value
8292
boolean enableExpectContinue = "TRUE".equalsIgnoreCase(enableInitParameter);
8393

84-
// Pass value to Milton
85-
httpManager.setEnableExpectContinue(enableExpectContinue);
86-
8794
LOG.debug("Set 'Enable Expect Continue' to {}", enableExpectContinue);
8895
}
96+
97+
@Override
98+
protected void service(HttpServletRequest req, HttpServletResponse resp)
99+
throws ServletException, IOException {
100+
delegate.service(req, resp);
101+
}
102+
103+
@Override
104+
public void destroy() {
105+
delegate.destroy();
106+
super.destroy();
107+
}
89108
}

extensions/webdav/src/test/java/org/exist/webdav/CDataIntergationTest.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@
2222

2323
package org.exist.webdav;
2424

25-
import com.bradmcevoy.http.exceptions.BadRequestException;
26-
import com.bradmcevoy.http.exceptions.ConflictException;
27-
import com.bradmcevoy.http.exceptions.NotAuthorizedException;
28-
import com.bradmcevoy.http.exceptions.NotFoundException;
29-
import com.ettrema.httpclient.*;
30-
import org.apache.http.impl.client.AbstractHttpClient;
25+
import io.milton.http.exceptions.BadRequestException;
26+
import io.milton.http.exceptions.ConflictException;
27+
import io.milton.http.exceptions.NotAuthorizedException;
28+
import io.milton.http.exceptions.NotFoundException;
29+
import io.milton.httpclient.*;
3130
import org.exist.TestUtils;
3231
import org.exist.test.ExistWebServer;
3332
import org.junit.AfterClass;
@@ -80,12 +79,11 @@ public void cdataWebDavApi() throws IOException, NotAuthorizedException, BadRequ
8079
builder.setServer("localhost");
8180
final int port = EXIST_WEB_SERVER.getPort();
8281
builder.setPort(port);
82+
builder.setUser(TestUtils.ADMIN_DB_USER);
83+
builder.setPassword(TestUtils.ADMIN_DB_PWD);
8384
builder.setRootPath("webdav/db");
8485
final Host host = builder.buildHost();
8586

86-
// workaround pre-emptive auth issues of Milton Client
87-
final AbstractHttpClient httpClient = (AbstractHttpClient)host.getClient();
88-
httpClient.addRequestInterceptor(new AlwaysBasicPreAuth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD));
8987

9088
final Folder folder = host.getFolder("/");
9189
assertNotNull(folder);

extensions/webdav/src/test/java/org/exist/webdav/CopyTest.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@
2222

2323
package org.exist.webdav;
2424

25-
import com.bradmcevoy.http.exceptions.BadRequestException;
26-
import com.bradmcevoy.http.exceptions.ConflictException;
27-
import com.bradmcevoy.http.exceptions.NotAuthorizedException;
28-
import com.bradmcevoy.http.exceptions.NotFoundException;
29-
import com.ettrema.httpclient.*;
30-
import org.apache.http.impl.client.AbstractHttpClient;
25+
import io.milton.http.exceptions.BadRequestException;
26+
import io.milton.http.exceptions.ConflictException;
27+
import io.milton.http.exceptions.NotAuthorizedException;
28+
import io.milton.http.exceptions.NotFoundException;
29+
import io.milton.httpclient.*;
3130
import org.exist.TestUtils;
3231
import org.exist.test.ExistWebServer;
3332
import org.junit.AfterClass;
@@ -90,12 +89,11 @@ private void copyDocument(final String srcDocName, final String srcDocContent, f
9089
builder.setServer("localhost");
9190
final int port = existWebServer.getPort();
9291
builder.setPort(port);
92+
builder.setUser(TestUtils.ADMIN_DB_USER);
93+
builder.setPassword(TestUtils.ADMIN_DB_PWD);
9394
builder.setRootPath("webdav/db");
9495
final Host host = builder.buildHost();
9596

96-
// workaround pre-emptive auth issues of Milton Client
97-
final AbstractHttpClient httpClient = (AbstractHttpClient)host.getClient();
98-
httpClient.addRequestInterceptor(new AlwaysBasicPreAuth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD));
9997

10098
final Folder folder = host.getFolder("/");
10199
assertNotNull(folder);

0 commit comments

Comments
 (0)