fix(doodstream): use current page format (form action + matching sess_id)

The 3.3.25 diagnostics captured the live upload page: doodstream moved the
upload server from a `srv_url` JS variable into the multipart form's action,
e.g. action="https://xxx.cloudatacdn.com/upload/01?SESSID", with a per-page
session token in the query that matches the page's hidden sess_id input. The
old parser found neither and fell through to the stale hardcoded node, which
returns an empty filecode.

- Parse the upload server from the form action (matched via the /upload/ path),
  un-escaping & in the query string.
- Refresh this.sessId from the SAME page (only on action match) so the
  multipart sess_id field matches the node URL's token; login-time and node
  tokens otherwise diverge. Keep the existing sessId if the input is absent.
- Keep the legacy ?op=upload_server JSON and srv_url paths as fallbacks; the
  fail-fast throw from 3.3.25 stays as the last resort.
- Tests: form-action parse, sess_id refresh, & un-escape (9 total).

Whether this fully resolves the uploads is for the next server logs to confirm;
both the node and sess_id fixes are individually correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-05-25 00:32:25 +02:00
parent 1e6bb27404
commit 18a875a764
2 changed files with 46 additions and 0 deletions

View File

@ -210,6 +210,31 @@ class DoodstreamUploader {
// Fallback: try fetching from upload page HTML // Fallback: try fetching from upload page HTML
const pageRes = await this._fetch(BASE_URL + '/?op=upload'); const pageRes = await this._fetch(BASE_URL + '/?op=upload');
const html = await pageRes.text(); const html = await pageRes.text();
// Current doodstream format: the upload server is the action of the
// multipart upload form, e.g.
// <form name="file" enctype="multipart/form-data"
// action="https://xxx.cloudatacdn.com/upload/01?SESSID" ...>
// <input type="hidden" name="sess_id" value="SESSID">
// The node is assigned per page-load and the action carries a session token
// in its query string that matches the page's hidden sess_id. We refresh
// this.sessId from THIS page so the multipart sess_id field matches the node
// URL — login-time and node tokens otherwise diverge and the upload comes
// back with an empty filecode.
const actionMatch = html.match(/action=["'](https?:\/\/[^"']+\/upload\/[^"']*)["']/i);
if (actionMatch) {
const url = actionMatch[1].replace(/&amp;/g, '&'); // un-escape HTML entities in query
const freshSess = html.match(/name=["']sess_id["'][^>]*value=["']([a-zA-Z0-9]+)["']/);
if (freshSess) {
this.sessId = freshSess[1];
} else {
_debugLog('upload_server: form action found but no sess_id on page; keeping existing sessId');
}
_debugLog(`upload_server: using form action node=${url} sess=${this.sessId}`);
return url;
}
// Legacy fallback: srv_url JS variable (older doodstream theme).
const srvMatch = html.match(/srv_url['":\s]+['"]?(https?:\/\/[^'">\s]+)['"]?/i); const srvMatch = html.match(/srv_url['":\s]+['"]?(https?:\/\/[^'">\s]+)['"]?/i);
if (srvMatch) return srvMatch[1]; if (srvMatch) return srvMatch[1];

View File

@ -87,6 +87,27 @@ test('getUploadServer: falls back to srv_url in upload-page HTML', async () => {
assert.equal(await up._getUploadServer(), 'https://node7.cloudatacdn.com/upload/01'); assert.equal(await up._getUploadServer(), 'https://node7.cloudatacdn.com/upload/01');
}); });
test('getUploadServer: parses current form-action node and refreshes sess_id from the same page', async () => {
const up = new DoodstreamUploader();
up.sessId = 'stale-from-login';
up._fetch = async (url) => {
if (/op=upload_server/.test(url)) return fakeRes('<html>not json</html>');
return fakeRes('<form name="file" enctype="multipart/form-data" action="https://n9.cloudatacdn.com/upload/01?FRESH123" method="post"><input type="hidden" name="sess_id" value="FRESH123"></form>');
};
const url = await up._getUploadServer();
assert.equal(url, 'https://n9.cloudatacdn.com/upload/01?FRESH123');
assert.equal(up.sessId, 'FRESH123'); // critical: form-field token must match the node URL token
});
test('getUploadServer: un-escapes &amp; in the form-action query string', async () => {
const up = new DoodstreamUploader();
up._fetch = async (url) => {
if (/op=upload_server/.test(url)) return fakeRes('<html>not json</html>');
return fakeRes('<form name="file" enctype="multipart/form-data" action="https://n9.cloudatacdn.com/upload/01?a=1&amp;b=2" method="post"></form>');
};
assert.equal(await up._getUploadServer(), 'https://n9.cloudatacdn.com/upload/01?a=1&b=2');
});
test('getUploadServer: throws (no silent dead fallback) when discovery fails', async () => { test('getUploadServer: throws (no silent dead fallback) when discovery fails', async () => {
const up = new DoodstreamUploader(); const up = new DoodstreamUploader();
up._fetch = async () => fakeRes('<html><body>login required</body></html>', { status: 200 }); up._fetch = async () => fakeRes('<html><body>login required</body></html>', { status: 200 });