mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-06 17:38:06 +00:00
fix(settings): harden locations and site login matching
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getProxyArgs, normalizeCustomProxy, useDownloadStore } from './useDownloadStore';
|
||||
import { getProxyArgs, getSiteLogin, normalizeCustomProxy, useDownloadStore } from './useDownloadStore';
|
||||
import { useSettingsStore } from './useSettingsStore';
|
||||
import * as ipc from '../ipc';
|
||||
|
||||
@@ -90,6 +90,25 @@ describe('useDownloadStore', () => {
|
||||
} as ReturnType<typeof useSettingsStore.getState>)).toBe('socks5://127.0.0.1:1080');
|
||||
});
|
||||
|
||||
it('matches site logins by host, wildcard host, path, and full URL patterns', () => {
|
||||
const settings = {
|
||||
siteLogins: [
|
||||
{ id: 'host', urlPattern: 'example.com', username: 'host' },
|
||||
{ id: 'wildcard', urlPattern: '*.cdn.example.com', username: 'wildcard' },
|
||||
{ id: 'broad', urlPattern: '*.example.com', username: 'broad' },
|
||||
{ id: 'path', urlPattern: 'secure.example.com/private/*', username: 'path' },
|
||||
{ id: 'url', urlPattern: 'https://downloads.example.net/releases/*', username: 'url' }
|
||||
]
|
||||
} as ReturnType<typeof useSettingsStore.getState>;
|
||||
|
||||
expect(getSiteLogin('https://example.com/file.zip', settings)?.id).toBe('host');
|
||||
expect(getSiteLogin('https://assets.cdn.example.com/file.zip', settings)?.id).toBe('wildcard');
|
||||
expect(getSiteLogin('https://secure.example.com/private/file.zip', settings)?.id).toBe('path');
|
||||
expect(getSiteLogin('https://downloads.example.net/releases/app.zip', settings)?.id).toBe('url');
|
||||
expect(getSiteLogin('https://secure.example.com/public/file.zip', settings)?.id).toBe('broad');
|
||||
expect(getSiteLogin('https://unrelated.example.org/public/file.zip', settings)).toBeNull();
|
||||
});
|
||||
|
||||
it('Start Queue dispatches exactly once for mixed dispatched/undispatched items', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
|
||||
@@ -124,24 +124,64 @@ export const getProxyArgs = async (settings: ReturnType<typeof useSettingsStore.
|
||||
return null;
|
||||
};
|
||||
|
||||
const escapeRegex = (value: string): string =>
|
||||
value.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
const wildcardToRegex = (pattern: string): RegExp =>
|
||||
new RegExp(`^${escapeRegex(pattern).replace(/\*+/g, '.*')}$`);
|
||||
|
||||
const patternSpecificity = (pattern: string): number =>
|
||||
pattern.replace(/\*/g, '').length;
|
||||
|
||||
const hostPatternScore = (pattern: string, host: string): number | null => {
|
||||
if (pattern.startsWith('*.')) {
|
||||
const suffix = pattern.substring(2);
|
||||
return host === suffix || host.endsWith(`.${suffix}`)
|
||||
? 1000 + patternSpecificity(pattern)
|
||||
: null;
|
||||
}
|
||||
|
||||
if (pattern.includes('*')) {
|
||||
return wildcardToRegex(pattern).test(host)
|
||||
? 1000 + patternSpecificity(pattern)
|
||||
: null;
|
||||
}
|
||||
|
||||
return host === pattern ? 2000 + patternSpecificity(pattern) : null;
|
||||
};
|
||||
|
||||
const urlPatternScore = (pattern: string, url: URL): number | null => {
|
||||
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(pattern)) {
|
||||
const normalizedPattern = pattern.toLowerCase().replace(/\/+$/, '');
|
||||
const normalizedUrl = url.toString().toLowerCase().replace(/\/+$/, '');
|
||||
return wildcardToRegex(normalizedPattern).test(normalizedUrl)
|
||||
? 4000 + patternSpecificity(normalizedPattern)
|
||||
: null;
|
||||
}
|
||||
|
||||
if (pattern.includes('/')) {
|
||||
const normalizedPattern = pattern.toLowerCase().replace(/^\/+/, '');
|
||||
const normalizedTarget = `${url.hostname}${url.pathname}`.toLowerCase().replace(/\/+$/, '');
|
||||
return wildcardToRegex(normalizedPattern).test(normalizedTarget)
|
||||
? 3000 + patternSpecificity(normalizedPattern)
|
||||
: null;
|
||||
}
|
||||
|
||||
return hostPatternScore(pattern, url.hostname.toLowerCase());
|
||||
};
|
||||
|
||||
export const getSiteLogin = (url: string, settings: ReturnType<typeof useSettingsStore.getState>) => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const host = urlObj.hostname.toLowerCase();
|
||||
let bestMatch: { login: typeof settings.siteLogins[number]; score: number } | null = null;
|
||||
for (const login of settings.siteLogins) {
|
||||
let pattern = login.urlPattern.toLowerCase().trim();
|
||||
if (pattern.startsWith('*.')) {
|
||||
const suffix = pattern.substring(2);
|
||||
if (host === suffix || host.endsWith('.' + suffix)) return login;
|
||||
} else if (pattern.includes('*')) {
|
||||
const collapsed = pattern.replace(/\*+/g, '*');
|
||||
const escaped = collapsed.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
||||
if (regex.test(host)) return login;
|
||||
} else if (host === pattern) {
|
||||
return login;
|
||||
const pattern = login.urlPattern.toLowerCase().trim();
|
||||
const score = pattern ? urlPatternScore(pattern, urlObj) : null;
|
||||
if (score !== null && (!bestMatch || score > bestMatch.score)) {
|
||||
bestMatch = { login, score };
|
||||
}
|
||||
}
|
||||
return bestMatch?.login ?? null;
|
||||
} catch (e) {}
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user