@samitouri / QOSami-HFS / commits / 182b8aef

fix: api.misc.http* functions mixed cookies of different hosts

Massimo Melina committed Feb 12, 2026 at 12:05 UTC 182b8aefc9624c11b5f9123af3edbc442ebb7e3d
2 files changed +25 -14
src/util-http.ts
+9 -7
@@ -25,7 +25,7 @@ export interface XRequestOptions extends https.RequestOptions {
25 body?: string | Buffer | Readable
26 proxy?: string // url format
27 // very basic cookie store
28 - jar?: Record<string, string>
28 + jar?: { [host: string]: { [cookieName: string]: string } }
29 noRedirect?: boolean
30 // throw for http-level errors. Default is true.
31 httpThrow?: boolean
@@ -47,10 +47,12 @@ export function httpStream(url: string, { body, proxy, jar, noRedirect, httpThro
47 if (!(body instanceof Readable))
48 options.headers['content-length'] ??= Buffer.byteLength(body)
49 }
50 - if (jar)
51 - options.headers.cookie = _.map(jar, (v,k) => `${k}=${v}; `).join('')
52 - + (options.headers.cookie || '') // preserve parameter
50 const { auth, ...parsed } = parse(url)
51 + const hostJar = jar && (jar[parsed.hostname || ''] ||= {})
52 + if (hostJar) {
53 + options.headers.cookie = _.map(hostJar, (v,k) => `${k}=${v}; `).join('')
54 + + (options.headers.cookie || '') // preserve parameter
55 + }
56 const proxyParsed = proxy ? parse(proxy) : null
57 Object.assign(options, _.pick(proxyParsed || parsed, ['hostname', 'port', 'path', 'protocol']))
58 if (auth) {
@@ -72,11 +74,11 @@ export function httpStream(url: string, { body, proxy, jar, noRedirect, httpThro
74 const proto = options.protocol === 'https:' ? https : http
75 const req = proto.request(options, res => {
76 console.debug("http responded", res.statusCode, "to", url)
75 - if (jar) for (const entry of res.headers['set-cookie'] || []) {
77 + if (hostJar) for (const entry of res.headers['set-cookie'] || []) {
78 const [, k, v] = /(.+?)=([^;]+)/.exec(entry) || []
79 if (!k) continue
78 - if (v) jar[k] = v
79 - else delete jar[k]
80 + if (v) hostJar[k] = v
81 + else delete hostJar[k]
82 }
83 if (!res.statusCode || httpThrow && res.statusCode >= 400)
84 return reject(new Error(String(res.statusCode), { cause: res }))
tests/test.ts
+16 -7
@@ -210,13 +210,6 @@ describe('basics', () => {
210 test('delete.no perm', req('/for-admins/', 405, { method: 'delete' }))
211 test('delete.need account', req(UPLOAD_ROOT + 'alfa.txt', 401, { method: 'delete'}))
212 test('rename.no perm', reqApi('rename', { uri: '/for-admins', dest: 'any' }, 403))
213 - test('of_disabled.cantLogin', () => login('of_disabled').then(() => { throw "in" }, () => {}))
214 - test('allow_net.canLogin', () => login(username))
215 - test('allow_net.cantLogin', () => {
216 - defaultBaseUrl = BASE_URL_127 // 127.0.0.1 is not allowed for this account
217 - return login(username).then(() => { throw "in" }, () => {})
218 - .finally(() => defaultBaseUrl = BASE_URL)
219 - })
213
214 test('create_folder.bad encoding', reqApi('comment', { uri: '%a' }, 400))
215 test('comment.bad encoding', reqApi('comment', { uri: '%a', comment: 'anything' }, 400))
@@ -242,6 +235,22 @@ describe('limits', () => {
235 after(() => rm(fn))
236 })
237
238 +describe('sessions', () => {
239 + test('of_disabled.cantLogin', () => login('of_disabled').then(() => { throw "in" }, () => {}))
240 + test('allow_net.canLogin', () => login(username))
241 + test('allow_net.cantLogin', () => {
242 + defaultBaseUrl = BASE_URL_127 // 127.0.0.1 is not allowed for this account
243 + return login(username).then(() => { throw "in" }, () => {})
244 + .finally(() => defaultBaseUrl = BASE_URL)
245 + })
246 + test('httpStream.jar isolates host cookies', async () => {
247 + const jar = {}
248 + await reqApi('loginSrp1', { username }, res => Boolean(res?.salt && res?.pubKey), { jar })()
249 + await reqApi('loginSrp2', { pubKey: '1', proof: '1' }, 409, { baseUrl: BASE_URL_127, jar })()
250 + await reqApi('loginSrp2', { pubKey: '1', proof: '1' }, 401, { jar })()
251 + })
252 +})
253 +
254 describe('accounts', () => {
255 before(() => login(username))
256 test('get_accounts', reqApi('get_accounts', {}, ({ list }) => _.find(list, { username }) && _.find(list, { username: 'admins' })))