fix(doodstream): web upload submits the live form's fields (not stale hardcoded)

Direct improvement to the login/web path (no API key needed): we were POSTing a
stale field set — sess_id + utype=reg + file — but doodstream's CURRENT upload
form dropped `utype` and added file_title / fakefilepc / submit_btn. Submitting
an incomplete/stale field set can make the CDN node accept the bytes but skip
the registration step (→ the empty result form with no fn). Now we parse the
live upload form (already fetched in _getUploadServer) and replicate ALL its
non-file fields faithfully — exactly what the browser submits — while keeping
sess_id (the fresh node token) and utype as a harmless compatibility extra.

- _parseUploadFormFields(html): pull every named input/button from the upload
  form, excluding the file input (streamed separately). Adapts to whatever
  fields doodstream uses now rather than hardcoding.
- upload() builds the multipart from those fields; minimal known-good fallback
  if the form wasn't parsed.
- Tests: real-form extraction (incl. file-input exclusion) + no-form safety. 183/183.

Low regression risk (superset of the previously-working fields). Whether it
resolves the large-file empty-form is for the server run; the API path
(3.3.31/32) remains the reliable route when a key is available/derivable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-05-28 22:28:24 +02:00
parent d24fd54e83
commit 9ae5d312e1
2 changed files with 67 additions and 4 deletions

View File

@ -247,7 +247,10 @@ class DoodstreamUploader {
} else { } else {
_debugLog('upload_server: form action found but no sess_id on page; keeping existing sessId'); _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}`); // Capture the form's real fields so upload() submits exactly what the
// browser would (file_title, submit_btn, …) instead of stale hardcoded ones.
this._uploadFormFields = this._parseUploadFormFields(html);
_debugLog(`upload_server: using form action node=${url} sess=${this.sessId} fields=${Object.keys(this._uploadFormFields).join(',')}`);
return url; return url;
} }
@ -269,6 +272,35 @@ class DoodstreamUploader {
); );
} }
/**
* Replicate the non-file fields of doodstream's CURRENT upload form so our
* POST matches what the browser actually submits. Doodstream dropped the old
* `utype` field and added file_title / fakefilepc / submit_btn; submitting a
* stale/incomplete field set can make the node accept the bytes but skip
* registration ( empty result form). We parse the live form rather than
* hardcode, so we track whatever fields doodstream uses now. The file input
* (type=file) is excluded the file is streamed separately.
*/
_parseUploadFormFields(html) {
const fields = {};
if (!html) return fields;
// Narrow to the upload form (its action points at a /upload/ node).
const formMatch = html.match(/<form[^>]*\baction=["'][^"']*\/upload\/[^"']*["'][\s\S]*?<\/form>/i);
const scope = formMatch ? formMatch[0] : html;
const re = /<(?:input|button)\b([^>]*)>/gi;
let m;
while ((m = re.exec(scope)) !== null) {
const attrs = m[1];
const typeM = attrs.match(/\btype=["']([^"']*)["']/i);
if (typeM && typeM[1].toLowerCase() === 'file') continue;
const nameM = attrs.match(/\bname=["']([^"']+)["']/i);
if (!nameM) continue;
const valM = attrs.match(/\bvalue=["']([^"']*)["']/i);
fields[nameM[1]] = valM ? valM[1] : '';
}
return fields;
}
/** /**
* Upload file using web session * Upload file using web session
*/ */
@ -285,10 +317,16 @@ class DoodstreamUploader {
// Build multipart form // Build multipart form
const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`; const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`;
// Build form parts // Build form parts. Submit the live form's fields (parsed in
// _getUploadServer) so our POST matches the browser; merge in sess_id (the
// fresh node token) and keep utype=reg as a harmless compatibility extra.
// Falls back to the minimal known-good set if the form wasn't parsed.
const formFields = { utype: 'reg', ...(this._uploadFormFields || {}) };
formFields.sess_id = this.sessId;
let preamble = ''; let preamble = '';
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="sess_id"\r\n\r\n${this.sessId}\r\n`; for (const [name, value] of Object.entries(formFields)) {
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="utype"\r\n\r\nreg\r\n`; preamble += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
}
const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeFileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`; preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeFileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`;

View File

@ -64,6 +64,31 @@ test('happy path: link in result page wins', async () => {
assert.equal(res.file_code, 'jjsuhr931ds9'); assert.equal(res.file_code, 'jjsuhr931ds9');
}); });
// --- _parseUploadFormFields: replicate the current upload form faithfully ---
test('_parseUploadFormFields extracts the real form fields and excludes the file input', () => {
const up = new DoodstreamUploader();
const html = `
<form name="file" enctype="multipart/form-data" action="https://uxg.cloudatacdn.com/upload/01?TOK" method="post">
<input type="hidden" name="sess_id" value="TOK">
<input name="file" type="file" size="30" id="filepc">
<input name="fakefilepc" class="d-none" type="text" id="fakefilepc">
<input type="text" name="file_title" class="form-control">
<button type="submit" name="submit_btn" class="btn">Upload</button>
</form>`;
const f = up._parseUploadFormFields(html);
assert.equal(f.sess_id, 'TOK');
assert.equal(f.fakefilepc, '');
assert.equal(f.file_title, '');
assert.ok('submit_btn' in f);
assert.ok(!('file' in f), 'the file input must be excluded (streamed separately)');
});
test('_parseUploadFormFields returns {} for markup without a form', () => {
const up = new DoodstreamUploader();
assert.deepEqual(up._parseUploadFormFields('<div>no form here</div>'), {});
assert.deepEqual(up._parseUploadFormFields(''), {});
});
// --- deriveApiKey: pull + validate the account API key from the web session --- // --- deriveApiKey: pull + validate the account API key from the web session ---
test('_extractApiKeyCandidates finds the key in an input value and ranks api-context first', () => { test('_extractApiKeyCandidates finds the key in an input value and ranks api-context first', () => {
const up = new DoodstreamUploader(); const up = new DoodstreamUploader();