Skip to content

Serve AtomPub through a Roller servlet - #198

Open
snoopdave wants to merge 5 commits into
roller-6.1.xfrom
atompub-roller-servlet
Open

snoopdave wants to merge 5 commits into
roller-6.1.xfrom
atompub-roller-servlet

Conversation

@snoopdave

@snoopdave snoopdave commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Maps /roller-services/app/* to a new RollerAtomServlet, a small subclass of Propono's AtomServlet, so Roller controls how AtomPub requests are handled before Propono sees them.

  • Honour webservices.enableAtomPub on every request. Until now only the service document checked the setting. With AtomPub off, the endpoint now answers 404, the same way the XML-RPC endpoint does.
  • Read Atom entry bodies with Roller's standard XML parser configuration. For a POST of Atom content or a PUT to an entry URI, the servlet reads the body (up to 10 MB), parses it with the same SafeSAXBuilder settings Roller uses elsewhere, and hands Propono a buffered copy. An entry that does not parse gets 400; an oversized one gets 413. Media uploads and reads are unchanged.
  • RollerAtomHandler's entry-URI rule moves into a static isEntryPath helper, so the servlet and the handler use the same test.

Testing

  • New RollerAtomServletTest (10 tests): disabled service, pass-through of reads and media, buffered forwarding of a normal entry, refused entries on POST and PUT, size limit, and the shared entry-path rule.
  • mvn -pl app test on JDK 11: 335 tests, 0 failures, 1 skipped.

Targets roller-6.1.x for 6.1.7. It will be cherry-picked to master after the 6.1.7 release.

Map /roller-services/app/* to a small RollerAtomServlet that extends
Propono's AtomServlet:

- Answer 404 while webservices.enableAtomPub is off, matching the XML-RPC
  endpoint. Previously only the service document checked the setting.
- Read Atom entry bodies (POST of Atom content, PUT to an entry URI) with
  Roller's standard XML parser configuration, cap them at 10 MB, and hand
  Propono a buffered copy. Unparseable entries get 400; oversized ones 413.

RollerAtomHandler's entry-URI test moves into a static helper so the
servlet and handler use the same rule.
@snoopdave snoopdave added the 6.1.7 label Oct 3, 2026

@snoopdave snoopdave left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR-Review: 1 inline comment posted.

@snoopdave

Copy link
Copy Markdown
Contributor Author

🐞Claude Issue: PR-Review: General Issues

The following issues were found but cannot be attached to a specific line in the diff:

  • Blocking: CHANGES.md on roller-6.1.x is not updated. Add a short 6.1.7 entry: AtomPub now honours webservices.enableAtomPub on every request (404 when off), and entry bodies have a 10 MB limit (413 when larger).
  • Important: No CI runs on this PR. On roller-6.1.x, main.yml and codeql-analysis.yml trigger only for master, so nothing on the branch is checked automatically. I ran RollerAtomServletTest locally on JDK 11: 10 tests, 0 failures. Consider adding roller-6.1.x to the workflow triggers on that branch before the 6.1.7 PRs merge.

@snoopdave

Copy link
Copy Markdown
Contributor Author

🤖Claude:

  • CHANGES.md: fixed by adding a 6.1.7 section with both behaviour changes: every AtomPub URL answers 404 while webservices.enableAtomPub is off, and entry bodies over 10 MB get 413 (not yet pushed).
  • CI: not fixed here. Run CI and CodeQL on roller-6.1.x #206 has since added roller-6.1.x to the CI and CodeQL triggers. This PR predates that change, so checks will start after it is rebased onto roller-6.1.x.

RollerAtomServlet now creates the AtomPub handler first and answers 401
when no user is authenticated, before buffering or parsing the entry. The
handler is passed to Propono through a request attribute, which
RollerAtomHandlerFactory returns, so authentication runs once per request.

Add a 6.1.7 CHANGES.md section for the AtomPub behaviour changes.
snoopdave and others added 2 commits October 4, 2026 08:26
…al entity in user-controlled data'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Fold the disabled-service read check into the write check, and drop the
media PUT, unauthenticated PUT and entry-path cases, which repeat paths
the remaining tests already cover.
@snoopdave

Copy link
Copy Markdown
Contributor Author

With those fixes, I think this PR is ready for merge.

@snoopdave

Copy link
Copy Markdown
Contributor Author

Manually tested on the 6.1.7 integration build (Tomcat 9, JDK 11, MySQL 8) with curl and basic auth. Pass.

  • With AtomPub on:
    • The service document, entry POST (201) and PUT (200), the entries feed, and media POST with Slug work.
    • A malformed entry, or one with an external entity or DTD, returns 400 for both POST and PUT. Nothing is fetched.
    • An 11 MB entry returns 413.
    • With no credentials or a wrong password, the response is 401, even for malformed or oversized bodies. So authentication runs before the body is read.
  • With AtomPub off, every URL returns 404 at once, with no restart.

Testing found some old MediaCollection bugs that this PR did not cause, such as listing /resources/default. They are fixed in #207.

@snoopdave snoopdave removed the Ready for Review Ready for review label Oct 4, 2026
Comment on lines +91 to +95
byte[] body = readBody(req.getInputStream());
if (body == null) {
sendText(res, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Entry is too large");
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already have commons-io as a transitive dependency. You could use it to replace readBody:

Suggested change
byte[] body = readBody(req.getInputStream());
if (body == null) {
sendText(res, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Entry is too large");
return;
}
// Read one byte past the limit, so an oversized body can be detected.
byte[] body = IOUtils.toByteArray(
BoundedInputStream.builder()
.setInputStream(req.getInputStream())
.setMaxCount(MAX_ENTRY_BYTES + 1L)
.get());
if (body.length > MAX_ENTRY_BYTES) {
sendText(res, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Entry is too large");
return;
}

In Java 9+ you also have InputStream.readNBytes()

Comment on lines +96 to +108
try {
// Propono reads the entry as UTF-8 text, so check the same text.
SafeSAXBuilder saxBuilder = new SafeSAXBuilder();
saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxBuilder.build(new InputStreamReader(
new ByteArrayInputStream(body), StandardCharsets.UTF_8));
} catch (JDOMException e) {
LOG.debug("Rejecting Atom entry that could not be parsed", e);
sendText(res, HttpServletResponse.SC_BAD_REQUEST, "Invalid Atom entry");
return;
}

@ppkarwasz ppkarwasz Oct 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JDOM builds a tree that is later discarded. You might use a SAXParser instead with an empty content handler. The following code uses Commons Secure XML 1.1.0 (which will be released today) to validate the stream:

        try {
            DefaultHandler2 doctypeRefuser = new DefaultHandler2() {
                @Override
                public void startDTD(String name, String publicId, String systemId)
                        throws SAXException {
                    throw new SAXException("DOCTYPE is not allowed in an Atom entry");
                }
            };
            SAXParser parser = SecureSAXParserFactory.newNSSAXParser();
            parser.setProperty("http://xml.org/sax/properties/lexical-handler", doctypeRefuser);
            parser.parse(new InputSource(new InputStreamReader(
                    new ByteArrayInputStream(body), StandardCharsets.UTF_8)), doctypeRefuser);
        } catch (SAXException e) {
            LOG.debug("Rejecting Atom entry that could not be parsed", e);
            sendText(res, HttpServletResponse.SC_BAD_REQUEST, "Invalid Atom entry");
            return;
        }

Comment on lines +55 to +56
/** Largest Atom entry body accepted, in bytes. Media uploads are not affected. */
static final int MAX_ENTRY_BYTES = 10 * 1024 * 1024;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this limit a little high form one Atom entry? The entry still need to fit in the database. Even counting for the overhead of XML, 128 KiB or 1 MiB should be enough for all users.

This value might also be configurable (webservices.atomPubMaxEntrySize?).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants