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:
parent
d24fd54e83
commit
9ae5d312e1
@ -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`;
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user