diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js index 47d786a..d9a6a2b 100644 --- a/lib/doodstream-upload.js +++ b/lib/doodstream-upload.js @@ -210,6 +210,31 @@ class DoodstreamUploader { // Fallback: try fetching from upload page HTML const pageRes = await this._fetch(BASE_URL + '/?op=upload'); const html = await pageRes.text(); + + // Current doodstream format: the upload server is the action of the + // multipart upload form, e.g. + //
+ // + // 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(/&/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); if (srvMatch) return srvMatch[1]; diff --git a/tests/doodstream-upload.test.js b/tests/doodstream-upload.test.js index ca1d895..8f315aa 100644 --- a/tests/doodstream-upload.test.js +++ b/tests/doodstream-upload.test.js @@ -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'); }); +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('not json'); + return fakeRes('
'); + }; + 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 & in the form-action query string', async () => { + const up = new DoodstreamUploader(); + up._fetch = async (url) => { + if (/op=upload_server/.test(url)) return fakeRes('not json'); + return fakeRes('
'); + }; + 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 () => { const up = new DoodstreamUploader(); up._fetch = async () => fakeRes('login required', { status: 200 });