Skip to content

Commit 3fe2f86

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 5819e00 commit 3fe2f86

File tree

13 files changed

+246
-148
lines changed

13 files changed

+246
-148
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: 20 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,15 @@ 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+
ensureAuthenticated();
240247
// submit
241248
XmldbURI resourceURI = existCollection.createFile(newName, is, length, contentType);
242249

@@ -276,11 +283,11 @@ public LockResult lock(LockTimeout timeout, LockInfo lockInfo)
276283
LOG.debug("'{}' -- {}", resourceXmldbUri, lockInfo.toString());
277284
}
278285

279-
return refreshLock(UUIDGenerator.getUUIDversion4());
286+
return refreshLock(UUIDGenerator.getUUIDversion4(), timeout);
280287
}
281288

282289
@Override
283-
public LockResult refreshLock(String token) throws NotAuthorizedException, PreConditionFailedException {
290+
public LockResult refreshLock(String token, LockTimeout timeout) throws NotAuthorizedException, PreConditionFailedException {
284291

285292
if (LOG.isDebugEnabled()) {
286293
LOG.debug("'{}' token='{}'", resourceXmldbUri, token);
@@ -315,7 +322,8 @@ public LockToken getCurrentLock() {
315322
* MovableResource
316323
* =============== */
317324
@Override
318-
public void moveTo(CollectionResource rDest, String newName) throws ConflictException {
325+
public void moveTo(CollectionResource rDest, String newName) throws ConflictException, NotAuthorizedException, BadRequestException {
326+
ensureAuthenticated();
319327

320328
if (LOG.isDebugEnabled()) {
321329
LOG.debug("Move '{}' to '{}' in '{}'", resourceXmldbUri, newName, rDest.getName());
@@ -335,7 +343,8 @@ public void moveTo(CollectionResource rDest, String newName) throws ConflictExce
335343
* ================ */
336344

337345
@Override
338-
public void copyTo(CollectionResource toCollection, String newName) {
346+
public void copyTo(CollectionResource toCollection, String newName) throws NotAuthorizedException, BadRequestException, ConflictException {
347+
ensureAuthenticated();
339348

340349
if (LOG.isDebugEnabled()) {
341350
LOG.debug("Move '{}' to '{}' in '{}'", resourceXmldbUri, newName, toCollection.getName());
@@ -357,7 +366,7 @@ public void copyTo(CollectionResource toCollection, String newName) {
357366

358367
@Override
359368
public void sendContent(OutputStream out, Range range, Map<String, String> params,
360-
String contentType) throws IOException, NotAuthorizedException, BadRequestException {
369+
String contentType) throws IOException, NotAuthorizedException, BadRequestException, NotFoundException {
361370

362371
try {
363372
XMLOutputFactory xf = XMLOutputFactory.newInstance();

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

Lines changed: 25 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)
@@ -326,6 +339,7 @@ public Date getCreateDate() {
326339

327340
@Override
328341
public void delete() throws NotAuthorizedException, ConflictException, BadRequestException {
342+
ensureAuthenticated();
329343
existDocument.delete();
330344
}
331345

@@ -374,7 +388,7 @@ public LockResult lock(LockTimeout timeout, LockInfo lockInfo)
374388
* ================ */
375389

376390
@Override
377-
public LockResult refreshLock(String token) throws NotAuthorizedException, PreConditionFailedException {
391+
public LockResult refreshLock(String token, LockTimeout timeout) throws NotAuthorizedException, PreConditionFailedException {
378392

379393
if (LOG.isDebugEnabled()) {
380394
LOG.debug("Refresh: {} token={}", resourceXmldbUri, token);
@@ -447,7 +461,8 @@ public LockToken getCurrentLock() {
447461
}
448462

449463
@Override
450-
public void moveTo(CollectionResource rDest, String newName) throws ConflictException {
464+
public void moveTo(CollectionResource rDest, String newName) throws ConflictException, NotAuthorizedException, BadRequestException {
465+
ensureAuthenticated();
451466

452467
if (LOG.isDebugEnabled()) {
453468
LOG.debug("moveTo: {} newName={}", resourceXmldbUri, newName);
@@ -468,7 +483,8 @@ public void moveTo(CollectionResource rDest, String newName) throws ConflictExce
468483
* ================ */
469484

470485
@Override
471-
public void copyTo(CollectionResource rDest, String newName) {
486+
public void copyTo(CollectionResource rDest, String newName) throws NotAuthorizedException, BadRequestException, ConflictException {
487+
ensureAuthenticated();
472488

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

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

Lines changed: 38 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;
@@ -253,6 +260,30 @@ protected String decodePath(String uri) {
253260
return path;
254261
}
255262

263+
/**
264+
* Ensure the authenticated subject is available for write operations.
265+
* In Milton 4.x, handlers may call resource methods on newly-created
266+
* resource objects that haven't gone through authenticate(). This method
267+
* pulls credentials from the current request's auth context as a fallback.
268+
*/
269+
protected void ensureAuthenticated() {
270+
if (subject == null) {
271+
final io.milton.http.Request req = io.milton.http.HttpManager.request();
272+
if (req != null && req.getAuthorization() != null) {
273+
final String authUser = req.getAuthorization().getUser();
274+
final String authPwd = req.getAuthorization().getPassword();
275+
if (authUser != null) {
276+
subject = existResource.authenticate(authUser, authPwd);
277+
if (subject != null) {
278+
existResource.setUser(subject);
279+
existResource.isInitialized = false;
280+
existResource.initMetadata();
281+
}
282+
}
283+
}
284+
}
285+
}
286+
256287
/* ========
257288
* Resource
258289
* ======== */
@@ -295,6 +326,9 @@ public Object authenticate(String username, String password) {
295326
return null;
296327
}
297328

329+
// Propagate subject to the underlying eXist resource
330+
existResource.setUser(subject);
331+
298332
// Guest is not allowed to access.
299333
Subject guest = brokerPool.getSecurityManager().getGuestSubject();
300334
if (guest.equals(subject)) {
@@ -314,6 +348,7 @@ public Object authenticate(String username, String password) {
314348

315349
@Override
316350
public boolean authorise(Request request, Method method, Auth auth) {
351+
ensureAuthenticated();
317352

318353
LOG.info("{} {} (write={})", method.toString(), resourceXmldbUri, method.isWrite);
319354

@@ -405,7 +440,7 @@ public Date getModifiedDate() {
405440
}
406441

407442
@Override
408-
public String checkRedirect(Request request) {
443+
public String checkRedirect(Request request) throws NotAuthorizedException, BadRequestException {
409444
return null;
410445
}
411446
}

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
}

0 commit comments

Comments
 (0)