Skip to content

Added support for the importExportRateLimiting setting, and Access-Control-Allow-Origin header added. Fixed the export of readonly pads.#142

Open
yanntrividic wants to merge 3 commits intoether:masterfrom
yanntrividic:master
Open

Added support for the importExportRateLimiting setting, and Access-Control-Allow-Origin header added. Fixed the export of readonly pads.#142
yanntrividic wants to merge 3 commits intoether:masterfrom
yanntrividic:master

Conversation

@yanntrividic
Copy link
Copy Markdown

As mentioned several times (#9, #30, #139), in order to use ep_markdown through the Etherpad API, we need to add the CORS headers to the request. The problem with only doing that, if I understood well, is that it exposes the server to DDOS attacks.

In order to circumvent the problem, the other import/export functionalities of Etherpad are capped through a variable set in the settings.json file: importExportRateLimiting. The implementation of this setting is done in the importexport.ts file of etherpad-lite.

This PR aims at implementing this limiter alongside the CORS headers, in order to be able to use the export function of ep_markdown through the API.

Pads in readonly mode weren't being exported due to a lack of conversion of the pad ID. It is now working.
@yanntrividic yanntrividic changed the title Added support for the importExportRateLimiting setting, and Access-Control-Allow-Origin header added. Added support for the importExportRateLimiting setting, and Access-Control-Allow-Origin header added. Fixed the export of readonly pads. Jul 4, 2024
@yanntrividic
Copy link
Copy Markdown
Author

It's been some time since I did this PR. I recently discovered that it was not possible to export pads in readonly mode, so I fixed it. If I could, I would have opened a new PR, but GitHub only allows one fork per project for the same user.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the ep_markdown export endpoint to be usable from browsers by adding a CORS header, while also applying Etherpad’s importExportRateLimiting to mitigate abuse. It also adjusts markdown export to work when the request uses a read-only pad ID.

Changes:

  • Add express-rate-limit and apply a rate limiter to the markdown export route using settings.importExportRateLimiting.
  • Add Access-Control-Allow-Origin: * to the markdown export response.
  • Resolve read-only pad IDs to real pad IDs before exporting markdown.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
package.json Adds express-rate-limit dependency to support request limiting.
express.js Adds limiter middleware and sets CORS header for markdown export route.
exportMarkdown.js Adds read-only pad ID resolution; modifies revision selection logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread exportMarkdown.js

const getPadMarkdown = async (pad, revNum) => {
const atext = revNum == null ? pad.atext : await pad.getInternalRevisionAText(revNum);
const atext = revNum == null|undefined ? pad.atext : await pad.getInternalRevisionAText(revNum);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

revNum == null|undefined uses the bitwise OR operator, which changes the condition to effectively revNum == 0 and will cause the wrong revision text to be exported. Use a proper null/undefined check (for example revNum == null) before deciding whether to call getInternalRevisionAText().

Suggested change
const atext = revNum == null|undefined ? pad.atext : await pad.getInternalRevisionAText(revNum);
const atext = revNum == null ? pad.atext : await pad.getInternalRevisionAText(revNum);

Copilot uses AI. Check for mistakes.
Comment thread exportMarkdown.js
Comment on lines +320 to +325
if(padId.startsWith("r.")) {
return await readOnlyManager.getPadId(padId);
} else {
return padId
}
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

New getPadIdIfReadOnly() block doesn't match the rest of this file's style (missing spaces after if, uses double quotes, missing semicolons, and missing trailing semicolon after the function). This will likely fail eslint-config-etherpad linting; please align it with the surrounding conventions (single quotes, spaces, semicolons).

Suggested change
if(padId.startsWith("r.")) {
return await readOnlyManager.getPadId(padId);
} else {
return padId
}
}
if (padId.startsWith('r.')) {
return await readOnlyManager.getPadId(padId);
} else {
return padId;
}
};

Copilot uses AI. Check for mistakes.
Comment thread express.js
Comment on lines +10 to +15
handler: (request) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +
`"${request.originalUrl}" for IP address ${request.ip}`);
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

The custom handler passed to express-rate-limit only logs and never sends a response or calls next(). When the rate limit is exceeded, this will override the default handler and can leave the request unhandled (client hangs or gets no 429 body). Update the handler to use the proper (req, res, next, options) signature and end the response (and/or delegate to the default handler) after logging.

Suggested change
handler: (request) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +
`"${request.originalUrl}" for IP address ${request.ip}`);
}
handler: (request, response, next, options) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +
`"${request.originalUrl}" for IP address ${request.ip}`);
}
options.handler(request, response, next, options);

Copilot uses AI. Check for mistakes.
Comment thread express.js
Comment on lines +18 to +24

app.use('/p/:padId/:revNum?/export/markdown', limiter);
app.get('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
(async () => {
const {padId, revNum} = req.params;
res.attachment(`${padId}.md`);
res.header('Access-Control-Allow-Origin', '*');
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

Access-Control-Allow-Origin is set only on the successful export response. If the rate limiter triggers (or an error is thrown before line 24), the response will not include the CORS header and browsers will still report a CORS failure. Consider setting the CORS header in a middleware that runs before the limiter/handler for this route (or ensure the limiter's handler also sets it).

Suggested change
app.use('/p/:padId/:revNum?/export/markdown', limiter);
app.get('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
(async () => {
const {padId, revNum} = req.params;
res.attachment(`${padId}.md`);
res.header('Access-Control-Allow-Origin', '*');
app.use('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
next();
}, limiter);
app.get('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
(async () => {
const {padId, revNum} = req.params;
res.attachment(`${padId}.md`);

Copilot uses AI. Check for mistakes.
JohnMcLear added a commit that referenced this pull request Apr 19, 2026
Expands the previous CORS-only change so that /p/:padId/export/markdown
and /p/:padId/:revNum/export/markdown now match the full set of
protections that Etherpad core applies to its native /export/:type
routes (src/node/hooks/express/importexport.ts):

- Rate limit both routes with the core `importExportRateLimiting`
  settings, using the shared `express-rate-limit` module. Without this,
  a caller can bypass export throttling by switching from pdf/doc/txt/
  html/etc. to markdown.
- Resolve readonly pad IDs (`r.*`) through ReadOnlyManager before
  loading the pad. Previously padManager.getPad('r.xxx') would create
  an empty pad under the readonly ID and the export returned nothing.

`express-rate-limit` is already shipped by Etherpad core; declared here
as a peerDependency so the requirement is explicit.

Refs #139 (CORS), #142 (prior attempt that bundled rate limit +
readonly fix + CORS — this commit brings its ideas into the current
TS-based codebase).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants