Skip to content

Commit

Permalink
feat: add external blocklist support
Browse files Browse the repository at this point in the history
  • Loading branch information
johackim committed Feb 25, 2024
1 parent f659e0b commit b8298ca
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 25 deletions.
91 changes: 91 additions & 0 deletions __tests__/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getTimeType,
createTimeout,
getRunningApps,
isWithinTimeRange,
isValidDistraction,
isDistractionBlocked,
} from '../src/utils';
Expand Down Expand Up @@ -142,6 +143,37 @@ test('Should get duration time type', () => {
expect(getTimeType('10h-18h')).toBe('interval');
});

test('Should block an app', async () => {
const config = { blocklist: [{ name: 'chromium' }], whitelist: [] };
jest.spyOn(fs, 'readFileSync').mockImplementation(() => JSON.stringify(config));

const isBlocked = isDistractionBlocked('chromium');

expect(isBlocked).toBe(true);
});

test('Should block an app with a time-based interval', async () => {
const currentDate = new Date('2021-01-01T12:00:00Z');
const config = { blocklist: [{ name: 'chromium', time: '0h-23h' }], whitelist: [] };
jest.spyOn(fs, 'readFileSync').mockImplementation(() => JSON.stringify(config));
jest.spyOn(global, 'Date').mockImplementation(() => currentDate);

const isBlocked = isDistractionBlocked('chromium');

expect(isBlocked).toBe(true);
});

test('Should not block an app with a time-based interval', async () => {
const currentDate = new Date('2021-01-01T23:30:00Z');
const config = { blocklist: [{ name: 'chromium', time: '0h-23h' }], whitelist: [] };
jest.spyOn(fs, 'readFileSync').mockImplementation(() => JSON.stringify(config));
jest.spyOn(global, 'Date').mockImplementation(() => currentDate);

const isBlocked = isDistractionBlocked('chromium');

expect(isBlocked).toBe(false);
});

test('Should block a specific subdomain', async () => {
const config = { blocklist: [{ name: 'www.example.com' }], whitelist: [] };
jest.spyOn(fs, 'readFileSync').mockImplementation(() => JSON.stringify(config));
Expand Down Expand Up @@ -239,3 +271,62 @@ test('Should remove a distraction from blocklist if timeout is reached and shiel

expect(readConfig(TEST_CONFIG_PATH).blocklist).toEqual([{ name: 'chromium' }]);
});

test('Should block domains from an external blocklist', async () => {
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
jest.spyOn(fs, 'statSync').mockImplementation(() => ({ mtimeMs: 0 }));
jest.spyOn(fs, 'readFileSync').mockImplementation((path) => {
if (path === '/etc/ulysse/config.json') {
return JSON.stringify({ blocklist: [], whitelist: [] });
}

if (path === '/etc/ulysse/blocklist.json') {
return JSON.stringify([{ name: 'facebook.com' }, { name: 'twitter.com' }]);
}

return '';
});

const isBlocked = isDistractionBlocked('twitter.com');

expect(isBlocked).toBe(true);
});

test('Should run isDistractionBlocked in less than 100ms with a large external blocklist', async () => {
jest.spyOn(fs, 'statSync').mockImplementation(() => ({ mtimeMs: 1 }));
jest.spyOn(fs, 'readFileSync').mockImplementation((path) => {
if (path === '/etc/ulysse/config.json') {
return JSON.stringify({ blocklist: [], whitelist: [] });
}

if (path === '/etc/ulysse/blocklist.json') {
return JSON.stringify(Array.from({ length: 500000 }, (_, i) => ({ name: `${i + 1}.com` })));
}

return '';
});

isDistractionBlocked('example.com');

const start = process.hrtime();
isDistractionBlocked('example.com');
const end = process.hrtime(start);

expect(end[1] / 1000000).toBeLessThan(100);
});

test('Should check if a time is within an interval', async () => {
const currentDate = new Date('2021-01-01T12:00:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => currentDate);

expect(isWithinTimeRange('0h-23h')).toBe(true);
expect(isWithinTimeRange('0h-19h')).toBe(true);
expect(isWithinTimeRange('20h-23h')).toBe(false);
});

test('Should check if a time is within an interval', async () => {
const currentDate = new Date('2021-01-01T23:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => currentDate);

expect(isWithinTimeRange('0h-23h')).toBe(false);
});
68 changes: 43 additions & 25 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
SOCKET_PATH,
} from './constants';

const cache = {};

const tryCatch = (fn, fallback = false, retry = 0) => (...args) => {
try {
return fn(...args);
Expand Down Expand Up @@ -221,37 +223,53 @@ export const whitelistDistraction = (distraction) => {
sendDataToSocket(config);
};

export const rootDomain = (domain) => domain.split('.').slice(-2).join('.');
export const getRootDomain = (domain) => domain.split('.').slice(-2).join('.');

/* eslint-disable-next-line complexity */
export const isDistractionBlocked = (distraction) => {
const { blocklist, whitelist } = readConfig();
const time = blocklist.find((d) => d.name === distraction || d.name === `*.${rootDomain(distraction)}`)?.time;

if (whitelist.some((d) => d.name === distraction)) return false;
if (whitelist.some((d) => d.name === `*.${rootDomain(distraction)}`)) return false;
export const isDistractionWhitelisted = (distraction) => {
const { whitelist } = readConfig();

if (isValidDomain(distraction) && blocklist.some((d) => d.name === '*.*')) return true;
if (whitelist.some((d) => d.name === distraction)) return true;
if (whitelist.some((d) => d.name === `*.${getRootDomain(distraction)}`)) return true;

if (getTimeType(time) === 'interval') {
const date = new Date();
const hour = date.getHours();
return false;
};

const [start, end] = time.split('-').map((t) => parseInt(t, 10));
export const appendExternalBlocklist = (blocklist = [], path = '/etc/ulysse/blocklist.json') => {
if (!fs.existsSync(path)) return blocklist;
const date = fs.statSync(path).mtimeMs;

return hour >= start && hour < end;
if (date !== cache[path]?.date) {
const externalBlocklist = JSON.parse(fs.readFileSync(path, 'utf8'));
const combinedBlocklist = new Set([...blocklist, ...externalBlocklist]);
cache[path] = { blocklist: Array.from(combinedBlocklist), date };
}

const isBlocked = blocklist.some(({ name }) => {
if (name.includes('*')) {
const [, domain] = name.split('*.');
return domain === rootDomain(distraction);
}
return cache[path]?.blocklist;
};

return name === distraction || name === rootDomain(distraction);
});
export const isWithinTimeRange = (time) => {
if (!time) return true;
if (getTimeType(time) !== 'interval') return false;

const [start, end] = time.split('-').map((t) => parseInt(t, 10));
const hour = new Date().getUTCHours();

return hour >= start && hour < end;
};

export const isDomainBlocked = (domain, rule, rootDomain) => {
if (!isValidDomain(domain)) return false;
return rule === '*.*' || rule === domain || rule === `*.${rootDomain}`;
};

export const isDistractionBlocked = (distraction) => {
if (isDistractionWhitelisted(distraction)) return false;

const config = readConfig();
const rootDomain = getRootDomain(distraction);
const blocklist = appendExternalBlocklist(config.blocklist);

return isBlocked;
return blocklist.some(({ name, time }) => (name === distraction || isDomainBlocked(distraction, name, rootDomain)) && isWithinTimeRange(time));
};

export const isSudo = () => !!process.env.SUDO_USER;
Expand All @@ -270,9 +288,9 @@ export const blockApps = () => {
const config = readConfig();

const blocklist = config.blocklist
.filter((d) => isValidApp(d.name))
.filter((d) => isDistractionBlocked(d.name))
.map(({ name }) => name);
.map(({ name }) => name)
.filter((name) => isValidApp(name))
.filter((name) => isDistractionBlocked(name));

const blockedApps = getRunningApps()
.filter((a) => blocklist?.includes(a.cmd) || blocklist?.includes(a.bin) || blocklist?.includes(a.name))
Expand Down

0 comments on commit b8298ca

Please sign in to comment.