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
32 changes: 8 additions & 24 deletions src/entrypoints/content/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { i18n } from '#imports'
import { defineContentScript } from 'wxt/utils/define-content-script'
import { Hosts, findBestWildcardMatch } from '@/utils/hosts.ts'
import { Hosts } from '@/utils/hosts.ts'

// TODO: Logging

let url: URL
let tabEnabled = false
let lastCreds: string | undefined

export default defineContentScript({
matches: ['*://*/*'],
Expand All @@ -27,32 +28,15 @@ export default defineContentScript({

async function onChanged(changes: Record<string, any>) {
// console.debug('content/index.ts - onChanged:', changes)
const exactItems = changes[url.host[0]] // NOTE: Lazy Typing... in changes
const wildcardItems = changes['*']

if (!exactItems && !wildcardItems) return

if (exactItems) {
const oldCreds = exactItems.oldValue?.[url.host]
const newCreds = exactItems.newValue?.[url.host]
if (oldCreds !== newCreds) {
// If exact match was removed, check if a wildcard still covers this host
if (!newCreds) {
const wildcard = findBestWildcardMatch(url.host, await Hosts.all())
return await processCreds(wildcard)
}
return await processCreds(newCreds)
}
}

if (wildcardItems) {
const oldWildcard = findBestWildcardMatch(url.host, wildcardItems.oldValue)
const newWildcard = findBestWildcardMatch(url.host, wildcardItems.newValue)
if (oldWildcard !== newWildcard) await processCreds(newWildcard)
}
// NOTE: Only these buckets can affect the current host (exact entries + wildcards)
if (!(url.host[0] in changes) && !('*' in changes)) return
const creds = await Hosts.get(url.host)
if (creds === lastCreds) return
await processCreds(creds)
}

async function processCreds(creds: any) {
lastCreds = creds
// console.debug('processCreds - tabEnabled:', tabEnabled, '- creds:', creds)
try {
if (creds) {
Expand Down
43 changes: 33 additions & 10 deletions src/utils/hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function validateHostname(hostname: string): string | undefined {
const segments = hostPart.split('.')
if (segments.length === 0 || segments.includes('')) return undefined
for (const segment of segments) {
if (segment === '*') continue
if (segment === '*' || segment === '**') continue
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(segment)) return undefined
}

Expand All @@ -92,11 +92,29 @@ export function matchesWildcard(host: string, pattern: string): boolean {

const hostParts = hostName.split('.')
const patternParts = patternName.split('.')
if (patternParts.length !== hostParts.length) return false

return patternParts.every((part, i) =>
part === '*' ? (hostParts[i]?.length ?? 0) > 0 : part === hostParts[i],
)
return matchSegments(hostParts, patternParts)
}

// NOTE: `*` matches a single label, `**` matches one or more labels
function matchSegments(hostParts: string[], patternParts: string[]): boolean {
const match = (hostIndex: number, patternIndex: number): boolean => {
if (patternIndex === patternParts.length) return hostIndex === hostParts.length
const part = patternParts[patternIndex]
if (part === '**') {
for (let end = hostIndex + 1; end <= hostParts.length; end++) {
if (match(end, patternIndex + 1)) return true
}
return false
}
if (hostIndex >= hostParts.length) return false
if (part === '*')
return (
(hostParts[hostIndex]?.length ?? 0) > 0 && match(hostIndex + 1, patternIndex + 1)
)
return hostParts[hostIndex] === part && match(hostIndex + 1, patternIndex + 1)
}
return match(0, 0)
}

export function findBestWildcardMatch(
Expand All @@ -113,12 +131,12 @@ function findBestWildcard(
if (!patterns) return undefined
let bestKey: string | undefined
let bestCreds: string | undefined
let bestSpecificity = Infinity
let bestSpecificity = -1
for (const [pattern, creds] of Object.entries(patterns)) {
if (!pattern.includes('*')) continue
if (matchesWildcard(host, pattern)) {
const specificity = countWildcards(pattern)
if (specificity < bestSpecificity) {
const specificity = wildcardSpecificity(pattern)
if (specificity > bestSpecificity) {
bestSpecificity = specificity
bestKey = pattern
bestCreds = creds
Expand All @@ -135,6 +153,11 @@ function parseHostPort(value: string): [string, string | undefined] {
: [value.slice(0, colon), value.slice(colon + 1)]
}

function countWildcards(pattern: string): number {
return (pattern.match(/\*/g) || []).length
// NOTE: Exact labels are most specific, then `*`, then `**`
function wildcardSpecificity(pattern: string): number {
return pattern.split('.').reduce((score, segment) => {
if (segment === '**') return score
if (segment === '*') return score + 1
return score + 2
}, 0)
}
6 changes: 6 additions & 0 deletions tests/test-validate-hostname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const validTests: [string, string][] = [
['localhost', 'localhost'],
['Example.COM', 'example.com'],
[' Example.COM ', 'example.com'],
['*.example.com', '*.example.com'],
['staging.**', 'staging.**'],
['staging.**.example.com', 'staging.**.example.com'],
['**.example.com', '**.example.com'],
['staging.**:8080', 'staging.**:8080'],
]

const invalidTests: string[] = [
Expand All @@ -37,6 +42,7 @@ const invalidTests: string[] = [
'`example.com',
'example.com:abc',
'example..com',
'example.***.com',
'.',
'-',
]
Expand Down
36 changes: 35 additions & 1 deletion tests/test-wildcard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { matchesWildcard } from '@/utils/hosts.ts'
import { matchesWildcard, findBestWildcardMatch } from '@/utils/hosts.ts'

const tests: [string, string, boolean][] = [
['sub.example.com', '*.example.com', true],
Expand All @@ -14,10 +14,44 @@ const tests: [string, string, boolean][] = [
['example.com', '*', false],
['sub.example.com:8080', '*.example.com:*', true],
['sub.example.com:8080', '*.example.com:9090', false],
['staging.a', 'staging.**', true],
['staging.a.b', 'staging.**', true],
['staging.a.b.c.d', 'staging.**', true],
['staging', 'staging.**', false],
['staging.a', 'staging.**.com', false],
['a.example.com', '**.example.com', true],
['a.b.example.com', '**.example.com', true],
['example.com', '**.example.com', false],
['a.b.example.com:8080', '**.example.com:*', true],
['a.example.com', 'a.**.com', true],
['a.b.c.example.com', 'a.**.com', true],
['a.example.com', 'a.**.net', false],
]

const bestTests: [string, Record<string, string>, string | undefined][] = [
['staging.a', { 'staging.**': 'user1:pass', 'staging.*': 'user2:pass' }, 'user2:pass'],
['staging.a.b', { 'staging.**': 'user1:pass' }, 'user1:pass'],
['staging', { 'staging.**': 'user1:pass' }, undefined],
['example.com', { '**.com': 'user1:pass' }, 'user1:pass'],
['a.example.com', { '**.com': 'user1:pass' }, 'user1:pass'],
[
'a.example.com',
{ '*.example.com': 'user2:pass', '**.example.com': 'user1:pass' },
'user2:pass',
],
]

for (const [host, pattern, expected] of tests) {
const result = matchesWildcard(host, pattern)
const status = result === expected ? '' : '⛔ FAIL ⛔'
console.log(`${pattern.padEnd(19)} ${expected ? '✅' : '❌'} ${host} ${status}`)
}

console.log('\nBest match:')
for (const [host, patterns, expected] of bestTests) {
const result = findBestWildcardMatch(host, patterns)
const status = result === expected ? '' : '⛔ FAIL ⛔'
console.log(
`${host.padEnd(24)} ${expected ?? 'none'.padEnd(10)} ${JSON.stringify(patterns)} ${status}`,
)
}