diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js index cbfeee6..bb40c3a 100644 --- a/lib/doodstream-upload.js +++ b/lib/doodstream-upload.js @@ -247,7 +247,10 @@ class DoodstreamUploader { } 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}`); + // 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; } @@ -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(/]*\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 */ @@ -285,10 +317,16 @@ class DoodstreamUploader { // Build multipart form 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 = ''; - preamble += `--${boundary}\r\nContent-Disposition: form-data; name="sess_id"\r\n\r\n${this.sessId}\r\n`; - preamble += `--${boundary}\r\nContent-Disposition: form-data; name="utype"\r\n\r\nreg\r\n`; + for (const [name, value] of Object.entries(formFields)) { + preamble += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`; + } 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`; diff --git a/tests/doodstream-upload.test.js b/tests/doodstream-upload.test.js index 0349d19..f7cfcb5 100644 --- a/tests/doodstream-upload.test.js +++ b/tests/doodstream-upload.test.js @@ -64,6 +64,31 @@ test('happy path: link in result page wins', async () => { 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 = ` +
+ + + + + +
`; + 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('
no form here
'), {}); + assert.deepEqual(up._parseUploadFormFields(''), {}); +}); + // --- 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', () => { const up = new DoodstreamUploader();