Commit b6f5bfbb authored by Xunnamius (Luna)'s avatar Xunnamius (Luna)

many updates

parent d1f7c437
This diff is collapsed.
/** @flow
* @description All higher-level extension event logic is here
* @description These are the foundational functions responsible for bridging Chrome's event system with our own
*/
import { extendDownloadItemInstance } from 'universe'
import { portEvent } from 'universe/ui'
import {
Debug,
extractOriginDomainFromURI,
JUDGEMENT_UNDECIDED,
} from 'universe'
import {
EventFrame,
FrameworkEventEmitter
EventFrameEmitter,
portMessageToEvent,
eventFrameToPortMessage,
} from 'universe/events'
import { Debug } from 'universe'
import type { Chrome } from 'universe'
const resultHandlerFactory = (eventName, context, port) => (downloadItem) => {
port.postMessage(portEvent(eventName, downloadItem));
};
declare var Port;
/**
* * See: https://developer.chrome.com/extensions/webRequest#type-RequestFilter
*/
const webRequestFilter = {
urls: [
'http://*/*',
......@@ -24,75 +29,55 @@ const webRequestFilter = {
]
};
// ? Essentially, we hook into four browser-level events here:
// ? - when the extension is first installed
// ? - when an http/s web request is started
// ? - when an http/s web request results in an error
// ? - when a download first starts
// ? - when a download finishes
export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object) => {
/**
* ? The following function hooks HASCHK into these browser-level events:
* ? - When the extension is first installed
* ? - When an http/s web request is started
* ? - When an http/s web request results in an error (debug only)
* ? - When a download first starts
* ? - When a download finishes
* ? - When a message is received from another component of the extension
*/
export default (oracle: EventFrameEmitter, chrome: Chrome, context: Object) => {
chrome.runtime.onConnect.addListener((port) => {
Debug.log('runtime.onConnect: ', port);
const handlerUnsafeResult = resultHandlerFactory('judgement.unsafe', context, port);
const handlerSafeResult = resultHandlerFactory('judgement.safe', context, port);
const handlerUnknownResult = resultHandlerFactory('judgement.unknown', context, port);
// ? Whenever an event is triggered, we send it out for any other
// ? interested components in the system to pick up on
const globalListener = oracle.addGlobalListener((eventFrame: EventFrame, args: Array<any>) => {
port.postMessage(eventFrameToPortMessage(eventFrame, args));
});
// ? When a port is destroyed for whatever reason (page refresh, popup
// ? closed, etc), remove the (dead) global listener associated with it
port.onDisconnect.addListener(() => {
Debug.log('(port.onDisconnect)');
oracle.removeListener('judgement.unsafe', handlerUnsafeResult);
oracle.removeListener('judgement.safe', handlerSafeResult);
oracle.removeListener('judgement.unknown', handlerUnknownResult);
Debug.log('(port disconnected)');
oracle.removeGlobalListener(globalListener);
});
// ? These fire when judgements are made about downloads and send them out
// ? to other components of the extension.
// ?
// ? Three events are made available:
// ? * judgement.safe a resource's content is as expected
// ? * judgement.unsafe a resource's content is mutated/corrupted
// ? * judgement.unknown a resource's content cannot be judged (ignored)
oracle.addListener('judgement.unsafe', handlerUnsafeResult);
oracle.addListener('judgement.safe', handlerSafeResult);
oracle.addListener('judgement.unknown', handlerUnknownResult);
// TODO: describe me too
port.onMessage.addListener(message => {
Debug.log('port.onMessage: ', message);
if(message.event.charAt(0) !== '.' && message.event == 'fetch')
{
let values = {};
message?.data.forEach((key) => {
values[key] = context[key];
});
port.postMessage(values);
}
else
{
oracle.emit(message.event.substring(1), ...message.data);
// ? Remember all HaschkEventPort emits are promises waiting for
// ? a response, but when we interact with internal events
// ? (e.g. .judgement.unsafe) there is no response! so this
// ? is just an empty response so the promise resolves.
port.postMessage('');
}
// ? This fires when we receive a message from another component of the
// ? extension. We translate the message into an event and emit it
port.onMessage.addListener(data => {
Debug.log('port.onMessage: ', data);
const { eventFrame, args } = portMessageToEvent(data);
oracle.emitIgnoreGlobalListeners(eventFrame.name, eventFrame, ...args);
});
});
// ? This event fires when the extension is first installed
chrome.runtime.onInstalled.addListener(details => {
Debug.log('runtime.onInstalled: ', details);
if(['install', 'update'].includes(details.reason))
chrome.tabs.create({ url: chrome.runtime.getURL('welcome.html') });
oracle.emit('startup', details);
});
// ? This event fires whenever a navigation event first begins
chrome.webRequest.onBeforeRequest.addListener(details => {
Debug.log('webRequest.onBeforeRequest: ', details);
// TODO
}, webRequestFilter);
// ? This event fires whenever a WebRequest error occurs
......@@ -107,7 +92,7 @@ export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object)
// ! even years), so we cannot trust that this event
chrome.downloads.onCreated.addListener(downloadItem => {
Debug.log('downloads.onCreated: ', downloadItem);
oracle.emit('download.incoming', new EventFrame(), downloadItem);
oracle.emit('download.incoming', downloadItem);
});
// ? This event fires when some download-related event changes
......@@ -116,9 +101,25 @@ export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object)
// ? Only trigger the moment a download completes
if(targetItem?.state?.current == 'complete') {
// ? We need to ask for the most up-to-date DownloadItem object
/**
* ? We ask for the most up-to-date DownloadItem instance and extend
* ? it with the following useful information:
* ? - originDomain {String} The OD of this download
* ? - requestId {Number} The Request ID associated with this download
* ? - requestStack {Array<Object>} A FILO stack of `details` objects
* ? - judgement {Boolean} What HASCHK thinks about this DownloadItem (this is added later)
*/
chrome.downloads.search({ id: targetItem.id }, ([ downloadItem ]) => {
oracle.emit('download.completed', new EventFrame(), extendDownloadItemInstance(downloadItem));
// TODO
const uri: string = downloadItem.referrer || downloadItem.url;
context;
if(!uri) throw new Error('cannot determine originDomain');
downloadItem.originDomain = extractOriginDomainFromURI(uri);
downloadItem.judgement = JUDGEMENT_UNDECIDED;
oracle.emit('download.completed', downloadItem);
});
}
});
......
/** @flow
* @description Most of the complex core HASCHK logic and functionality is here
* @description Most of the core HASCHK logic and functionality is implemented here
*/
import http from 'axios'
import ParsedUrl from 'url-parse'
import { EventFrameEmitter } from 'universe/events'
import type { Chrome } from 'universe'
import {
bufferToHex,
......@@ -14,84 +16,54 @@ import {
JUDGEMENT_SAFE,
} from 'universe'
import {
DownloadEventFrame,
FrameworkEventEmitter
} from 'universe/events'
export default (oracle: EventFrameEmitter, chrome: Chrome, context: Object) => {
// ? This is the NAH vs AH core "judgement" logic
oracle.addListener('download.completed', async (e, downloadItem) => {
// TODO
let authedHash: ?string;
let nonauthedHash: ?string;
let authedHashRaw: ?string;
import type { Chrome } from 'universe'
// ? Since it's finished downloading, grab the file's data
const $file = await http.get(`file://${downloadItem.filename}`, { responseType: 'arraybuffer' });
export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object) => {
oracle.addListener('download.incoming', async () => {
const eventFrame = new DownloadEventFrame(oracle, context);
// ? Hash file data with proper algorithm
// flow-disable-line
nonauthedHash = bufferToHex(await crypto.subtle.digest('SHA-256', $file.data));
// ? If the event was ended prematurely, assume downloadItem was handled
// ? elsewhere in the event flow
if(!eventFrame.stopped)
eventFrame.finish();
});
// ? Determine resource identifier and prepare for DNS request
const resourcePath = (new ParsedUrl(downloadItem.url, {})).pathname;
const resourceIdentifier = bufferToHex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(resourcePath)));
const outputLength = parseInt(HASHING_ALGORITHM);
// ? This is the NAH vs AH core "judgement" logic
oracle.addListener('download.completed', (e, downloadItem) => {
if(context.handledDownloadItems.has(downloadItem.id))
return;
(async (downloadItem) => {
let authedHash: ?string;
let nonauthedHash: ?string;
let authedHashRaw: ?string;
let completed = false;
try {
// ? Since it's finished downloading, grab the file's data
const $file = await http.get(`file://${downloadItem.filename}`, { responseType: 'arraybuffer' });
// ? Hash file data with proper algorithm
// flow-disable-line
nonauthedHash = bufferToHex(await crypto.subtle.digest('SHA-256', $file.data));
// ? Determine resource identifier and prepare for DNS request
const resourcePath = (new ParsedUrl(downloadItem.url, {})).pathname;
const resourceIdentifier = bufferToHex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(resourcePath)));
const outputLength = parseInt(HASHING_ALGORITHM);
if(!resourceIdentifier || resourceIdentifier.length != outputLength)
throw new Error('failed to hash resource identifier');
const [ riLeft, riRight ] = [
resourceIdentifier.slice(0, outputLength / 2),
resourceIdentifier.slice(outputLength / 2, outputLength)
];
// ? Make https-based DNS request
const targetDomain = downloadItem.originDomain;
const $authedHash = await http.get(GOOGLE_DNS_HTTPS_RI_FN(riLeft, riRight, targetDomain));
authedHashRaw = !$authedHash.data.Answer ? '<no answer>' : $authedHash.data.Answer.slice(-1)[0].data;
authedHash = authedHashRaw.replace(/[^0-9a-f]/gi, '');
completed = true;
}
catch(error) {
oracle.emit('error', error);
}
// ! We want to make sure we don't catch any errors from oracle.emit
if(completed) {
if(!authedHash || !authedHashRaw)
throw new TypeError('unexpected null type encountered');
// ? Compare DNS result (auth) with hashed local file data (nonauthed)
if(authedHash.length !== authedHashRaw.length - 2)
e.judgeUnknown(downloadItem);
else
authedHash !== nonauthedHash ? e.judgeUnsafe(downloadItem) : e.judgeSafe(downloadItem);
}
})(downloadItem);
if(!resourceIdentifier || resourceIdentifier.length != outputLength)
throw new Error('failed to hash resource identifier');
const [ riLeft, riRight ] = [
resourceIdentifier.slice(0, outputLength / 2),
resourceIdentifier.slice(outputLength / 2, outputLength)
];
// ? Make https-based DNS request
const targetDomain = downloadItem.originDomain;
const $authedHash = await http.get(GOOGLE_DNS_HTTPS_RI_FN(riLeft, riRight, targetDomain));
authedHashRaw = !$authedHash.data.Answer ? '<no answer>' : $authedHash.data.Answer.slice(-1)[0].data;
authedHash = authedHashRaw.replace(/[^0-9a-f]/gi, '');
if(!authedHash || !authedHashRaw)
throw new TypeError('unexpected null type encountered');
// ? Compare DNS result (auth) with hashed local file data (nonauthed)
if(authedHash.length !== authedHashRaw.length - 2)
oracle.emit('judgement.unknown', downloadItem);
else
oracle.emit(`judgement.${authedHash !== nonauthedHash ? 'unsafe' : 'safe'}`, downloadItem);
});
oracle.addListener('judgement.unknown', downloadItem => {
// TODO
context.judgedDownloadItems.push({
downloadItem: downloadItem,
judgement: JUDGEMENT_UNKNOWN
......@@ -99,6 +71,7 @@ export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object)
});
oracle.addListener('judgement.safe', downloadItem => {
// TODO
context.judgedDownloadItems.push({
downloadItem: downloadItem,
judgement: JUDGEMENT_SAFE
......@@ -106,6 +79,7 @@ export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object)
});
oracle.addListener('judgement.unsafe', downloadItem => {
// TODO
context.judgedDownloadItems.push({
downloadItem: downloadItem,
judgement: JUDGEMENT_UNSAFE
......
/** @flow
* @description All HASCHK UI event hooks go here, including for popup/options
* @description All HASCHK UI logic goes here (i.e. popup, options)
*/
// TODO: need to move this type of documentation into the wiki
// ? `oracle` emits the events that you should be hooking into. Feel free to add
// ? more events as they become necessary. Note that new `download.complete`
// ? events must be added with oracle::prependListener().
// ? Currently, events include:
// ? * core.init the extension is loaded by chrome (once) () => {}
// ? * download.incoming a new download has been observed async (e, downloadItem) => {}
// ? * download.suspiciousOrigin a new download has been observed async (e, downloadItem) => {}
// ? * download.completed a download has completed; must be prepended async (e, downloadItem) => {}
// ? * judgement.safe a resource's content is as expected (downloadItem) => {}
// ? * judgement.unsafe a resource's content is mutated/corrupted (downloadItem) => {}
// ? * judgement.unknown a resource's content cannot be judged (downloadItem) => {}
// ? * error a new error event has occurred (err) => {}
// ? Extension event flows:
// ? 1. Chrome enables extension (only once): => core.init
// ? 2. User triggered download: => download.incoming => download.suspiciousOrigin -?-> #5
// ? 3. Triggered (unhandled) download finishes: => download.completed => #5
// ? 4. Some error occurs: => error
// ? 5. A judgement is rendered: => judgement.safe|unsafe|unknown
// ? Note: at any point during flows #2/3, the flow can be cancelled (i.e.
// ? remaining event handlers not processed) after which one of the
// ? `judgement.*` events should be triggered. This is immediate (i.e. the rest
// ? of the event stack is skipped). All events triggered in flows #2/3 receive
// ? an EventFrame instance (e) that allows access to HASCHK's internals, i.e.
// ? via ::judgeUnsafe(), ::judgeSafe(), etc.
// ? All oracle events receive a modified downloadItem object with the following
// ? extra props:
// ? * downloadItem.originDomain the DNS lookup base domain
// ? Note that a `downloadItem` object is not guaranteed to have any properties
// ? of the DownloadItem extension API except ::OriginDomain. Do an existence
// ? check before trying to use them. See the Extensions API for more details.
import { setBadge } from 'universe/ui'
import { FrameworkEventEmitter } from 'universe/events'
import { EventFrameEmitter } from 'universe/events'
import { Debug } from 'universe'
import type { Chrome } from 'universe'
export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object) => {
export default (oracle: EventFrameEmitter, chrome: Chrome, context: Object) => {
// ? This is our generic error handler that fires whenever an error occurs
oracle.addListener('error', err => {
oracle.addListener('error', (errorFrame, exception, errorArgs) => {
Debug.log(`ErrorFrame:`, errorFrame);
Debug.log(`Exception object:`, exception);
Debug.log(`ErrorArgs:`, errorArgs);
// TODO
setBadge(chrome)('ERR', '#000');
console.error(`HASCHK ERROR: ${err}`);
console.error(`HASCHK ERROR: ${exception}`);
});
// ? This event fires whenever a new download is observed
oracle.addListener('download.incoming', async (e, downloadItem) => {
// TODO: add the new download to popup UI (tell popup UI to update)
// ! Note how this function is async, which means you can await Promises
Debug.log(`file "${downloadItem.filename}" incoming`);
Debug.log(`file incoming from ${downloadItem.finalUrl}`);
});
// ? This event fires whenever haschk decides it cannot judge a download
oracle.addListener('judgement.unknown', downloadItem => {
// TODO
setBadge(chrome)(' ', '#D0D6B5');
Debug.log(`file "${downloadItem.filename}" judgement: UNKNOWN`);
});
// ? This event fires whenever haschk decides a download is safe
oracle.addListener('judgement.safe', downloadItem => {
// TODO
setBadge(chrome)(' ', '#6EEB83');
Debug.log(`file "${downloadItem.filename}" judgement: SAFE`);
});
// ? This event fires whenever haschk decides a download is NOT safe
oracle.addListener('judgement.unsafe', downloadItem => {
// TODO
setBadge(chrome)(' ', '#FF3C38');
Debug.log(`file "${downloadItem.filename}" judgement: UNSAFE`);
});
oracle.addListener('ui.clear', () => {
// TODO
setBadge(chrome)('');
context.judgedDownloadItems = [];
});
};
......@@ -3,18 +3,39 @@
* @name Background
*/
import {
Debug,
FRAMEWORK_EVENTS
} from 'universe'
import { EventEmitter } from 'universe/events'
import { Debug } from 'universe'
import { EventFrameEmitter } from 'universe/events'
import registerChromeEvents from 'components/background/events.chrome'
import registerCoreEvents from 'components/background/events.core'
import registerUIEvents from 'components/background/events.ui'
const oracle = new EventEmitter(FRAMEWORK_EVENTS);
// ? `oracle` emits the events that you should be hooking into. Feel free to add
// ? more events as they become necessary.
// ? All extension events:
// ? * startup The extension is loaded by chrome (once) async (EventFrame, Details) => {}
// ? * download.incoming A new download has been observed async (EventFrame, DownloadItem*) => {}
// ? * download.completed A download has completed; must be prepended async (EventFrame, DownloadItem) => {}
// ? * judgement.safe A resource's content is as expected async (EventFrame, DownloadItem) => {}
// ? * judgement.unsafe A resource's content is mutated/corrupted async (EventFrame, DownloadItem) => {}
// ? * judgement.unknown A resource's content cannot be judged async (EventFrame, DownloadItem) => {}
// ? * error An error event occurred (NOT `await`-ed!) (EventFrame, Exception) => {}
// ? * popup.ui.clear Clear the entries in the popup ui async (EventFrame) => {}
// ? Main extension event flow (excluding UI updates, options, ports):
// ? 1. Chrome enables extension (only once): => startup
// ? 2. User triggered download: => download.incoming => #3
// ? 3. Triggered (unhandled) download finishes: => download.completed => #5
// ? 4. Some error occurs: => error
// ? 5. A judgement is rendered: => judgement.safe | judgement.unsafe | judgement.unknown
// ? * Note that, for listeners attached to the `download.incoming` event, the
// ? `DownloadItem` instance is missing many of its properties, including
// ? HASCHK-specific extensions (see `universe/index.js`). Do not expect them to
// ? exist until the `download.complete` event and others later in the flow.
const oracle = new EventFrameEmitter();
const context = {
// ? (download) id -> DownloadItem
downloadItems: new Map(),
......
/** @flow
* @description Options and configuration for HASCHK plugin
* @name Options
* @description Extension configuration options UI is implemented here
*/
import './index.css'
......
/** @flow
* @description HASCHK popup functionality
* @name Popup
* @description HASCHK's popup functionality is implemented here
*/
import './index.css'
import {
HaschkEventPort,
guaranteeElementById
guaranteeElementById,
} from 'universe/ui';
import {
JUDGEMENT_UNKNOWN,
JUDGEMENT_UNSAFE,
JUDGEMENT_SAFE
JUDGEMENT_SAFE,
JUDGEMENT_UNDECIDED,
} from 'universe';
declare var chrome:any;
const bridge = new HaschkEventPort(chrome);
const downloadList = guaranteeElementById('downloadItems');
// TODO: all of this
// TODO: ensure bugs are fixed and UI meets specification outlined at
// TODO: https://github.com/morty-c137-prime/HASCHK/issues/31
const downloadList = guaranteeElementById('downloadItems');
const appendDownloadToDownloadList = (downloadItem: any, judgement: string) => {
// flow-disable-line
let elem: HTMLElement = document.createElement('li');
elem.setAttribute('id', downloadItem.id);
elem.innerHTML = `#${downloadItem.id}: ${downloadItem.filename} <span class=${judgement}>[${
......@@ -34,7 +30,7 @@ const appendDownloadToDownloadList = (downloadItem: any, judgement: string) => {
downloadList.insertBefore(elem, downloadList.childNodes[0]);
};
window.onload = async () => {
/* window.onload = async () => {
await bridge.emit('fetch', 'judgedDownloadItems').then((res) => {
res.judgedDownloadItems.forEach((download) => {
appendDownloadToDownloadList(download.downloadItem, download.judgement);
......@@ -52,7 +48,7 @@ bridge.on('judgement.safe', (downloadItem) => {
bridge.on('judgement.unknown', (downloadItem) => {
appendDownloadToDownloadList(downloadItem, JUDGEMENT_UNKNOWN);
});
}); */
// ??
// ?? Demo/development UI components
......@@ -66,7 +62,7 @@ bridge.on('judgement.unknown', (downloadItem) => {
// Object.keys(res.judgedDownloadItems).forEach((id) =>
// {
// let item = res.judgedDownloadItems[id];
// // flow-disable-line
//
// let download = document.createElement('li');
// download.setAttribute('id', item.downloadItem.id);
// download.innerHTML = `#${item.downloadItem.id}: ${item.downloadItem.filename} [${
......@@ -98,7 +94,7 @@ bridge.on('judgement.unknown', (downloadItem) => {
// });
// });
guaranteeElementById('clear').addEventListener('click', () => {
/* guaranteeElementById('clear').addEventListener('click', () => {
bridge.emit('.ui.clear');
downloadList.innerHTML = '';
});
}); */
/* @flow */
import InterruptibleEventFrame from './InterruptibleEventFrame'
// TODO: document me!
export default class DownloadEventFrame extends InterruptibleEventFrame {}
/* @flow */
// TODO: document me!
/**
* This class represents a default event frame. It is similar in intent to the
* DOM's event interface.
*
* * See also: https://developer.mozilla.org/en-US/docs/Web/API/Event
*/
export default class EventFrame {
name: string = '<unknown>';
stopped = false;
finished = false;
_finished: any;
......@@ -14,26 +19,32 @@ export default class EventFrame {
this._finishedFn = finishedFn || (() => {});
}
continue(...args: Array<any>) {
this._continueFn(...args);
/**
* Interrupt the event loop. The `continue()` and `finish()` methods will
* not be called (but can be called manually).
*/
stop() {
return this.stopped = true;
}
/**
* Stop the event from finishing. The finish() method will become a noop.
* This method is called after each registered listener is run so long as
* `stopped` remains false.
*
* @param {*} [args]
*/
stop() {
this.stopped = true;
continue(...args: Array<any>) {
return this._continueFn(...args);
}
// ? Should always be called eventually; is idempotent
/**
* Finish the event after it has been passed around. This should always be
* called eventually. This method is idempotent.
* This method is called automatically after all listeners are finished if
* `stopped` is false. Also, this method is idempotent.
*
* @param {...any} args
* @param {*} [args]
*/
finish(...args: Array<any>) {
if(!this.stopped && !this.finished)
if(!this.finished)
{
this.finished = true;
this._finished = this._finishedFn(...args);
......
/* @flow */
import EventEmitter from 'eventemitter3'
import { EventFrame } from 'universe/events'
import type { ListenerFn } from 'universe/events'
/**
* Unwinds the event loop so that each event listener is await-ed
* "synchronously".
*
* @param {EventFrameEmitter} emitter Which emitter we're emitting with
* @param {Number} index Where we are in the array of listeners
* @param {Array<ListenerFn>} listeners Said array of listeners
* @param {EventFrame} eventFrame The EventFrame instance
* @param {*} [args] Any arguments passed on to the listeners
*/
const asyncEmit = async (emitter: EventFrameEmitter,
index: number,
listeners: Array<ListenerFn>,
eventFrame: EventFrame,
args: Array<any>) =>
{
const listener: ?ListenerFn = listeners[index];
if(listener)
{
if(listener.$once)
emitter.removeListener(eventFrame.name, listener, undefined, true);
await listener(eventFrame, ...args);
!eventFrame.stopped && await asyncEmit(emitter, index + 1, listeners, eventFrame, args);
}
};
/**
* Modify a listener to interrupt the event loop when EventFrames are
* `stop()`-ed
*
* @param {ListenerFn} listener
* @param {boolean} [once=false]
*/
const asyncifyListener = (listener: ListenerFn, once: ?boolean) => {
const listenerActual = listener;
listener = async (eventFrame: EventFrame, ...args: Array<any>) => {
await listenerActual(eventFrame, ...args);
!eventFrame.stopped && await eventFrame.