Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion packages/core/src/connectors/capabilities/cookies-chrome.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,60 @@
import { describe, it, expect } from 'vitest'
import { makeChromeCookiesCapability } from './cookies-chrome.js'
import { makeChromeCookiesCapability, getMatchingHostKeys } from './cookies-chrome.js'
import { SyncError, SyncErrorCode } from '@spool/connector-sdk'

describe('getMatchingHostKeys', () => {
it('matches host-only and same-host domain cookies', () => {
expect(getMatchingHostKeys('reddit.com')).toEqual([
'reddit.com',
'.reddit.com',
])
})

it('matches parent domain cookies for subdomain requests', () => {
expect(getMatchingHostKeys('www.reddit.com')).toEqual([
'www.reddit.com',
'.www.reddit.com',
'.reddit.com',
])
})

it('walks all parent labels for deep subdomains', () => {
expect(getMatchingHostKeys('a.b.example.co.uk')).toEqual([
'a.b.example.co.uk',
'.a.b.example.co.uk',
'.b.example.co.uk',
'.example.co.uk',
'.co.uk',
])
})

it('does not walk into a bare TLD', () => {
const keys = getMatchingHostKeys('reddit.com')
expect(keys).not.toContain('.com')
expect(keys).not.toContain('com')
})

it('lower-cases the input host', () => {
expect(getMatchingHostKeys('WWW.Reddit.COM')).toEqual([
'www.reddit.com',
'.www.reddit.com',
'.reddit.com',
])
})

it('strips a leading dot from the input', () => {
expect(getMatchingHostKeys('.reddit.com')).toEqual([
'reddit.com',
'.reddit.com',
])
})

it('returns empty for single-label or empty hosts', () => {
expect(getMatchingHostKeys('localhost')).toEqual([])
expect(getMatchingHostKeys('')).toEqual([])
})
})

describe('makeChromeCookiesCapability', () => {
it('returns a capability with a get method', () => {
const cap = makeChromeCookiesCapability()
Expand Down
38 changes: 32 additions & 6 deletions packages/core/src/connectors/capabilities/cookies-chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,33 @@ interface RawCookieFull {
is_httponly: string
}

function queryAllCookiesForDomain(
/**
* Enumerate every Chrome `host_key` value that should match a request to `host`
* per RFC 6265 §5.1.3. Chrome stores host-only cookies under the bare hostname
* and domain cookies under `.parent.example.com`; a request to `www.example.com`
* must see cookies at `www.example.com`, `.www.example.com`, and `.example.com`
* but not anything scoped to a sibling (`.other.example.com`) or a TLD alone.
*/
export function getMatchingHostKeys(host: string): string[] {
const normalized = host.toLowerCase().replace(/^\./, '')
if (!normalized || !normalized.includes('.')) return []

const keys = [normalized, `.${normalized}`]
let cur = normalized
while (true) {
const idx = cur.indexOf('.')
if (idx < 0) break
const parent = cur.substring(idx + 1)
if (!parent.includes('.')) break
keys.push(`.${parent}`)
cur = parent
}
return keys
}

function queryAllCookiesForHost(
dbPath: string,
domain: string,
host: string,
): { cookies: RawCookieFull[]; dbVersion: number } {
if (!existsSync(dbPath)) {
throw new SyncError(
Expand All @@ -134,9 +158,12 @@ function queryAllCookiesForDomain(
)
}

const safeDomain = domain.replace(/'/g, "''")
const keys = getMatchingHostKeys(host)
if (keys.length === 0) return { cookies: [], dbVersion: 0 }

const quoted = keys.map(k => `'${k.replace(/'/g, "''")}'`).join(',')
// Fetch cookies and DB version in one sqlite3 invocation to avoid double process spawn
const sql = `SELECT name, host_key, path, hex(encrypted_value) as encrypted_value_hex, value, expires_utc, is_secure, is_httponly, (SELECT value FROM meta WHERE key='version') as db_version FROM cookies WHERE host_key LIKE '%${safeDomain}';`
const sql = `SELECT name, host_key, path, hex(encrypted_value) as encrypted_value_hex, value, expires_utc, is_secure, is_httponly, (SELECT value FROM meta WHERE key='version') as db_version FROM cookies WHERE host_key IN (${quoted});`

const output = runSqliteQuery(dbPath, sql)

Expand Down Expand Up @@ -190,8 +217,7 @@ export function makeChromeCookiesCapability(): CookiesCapability {
const key = getMacOSChromeKey()

const host = domainFromUrl(query.url)
const dotHost = host.startsWith('.') ? host : `.${host}`
const result = queryAllCookiesForDomain(dbPath, dotHost)
const result = queryAllCookiesForHost(dbPath, host)

const cookies: Cookie[] = []
for (const raw of result.cookies) {
Expand Down