Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ElectronBlocker abstraction #180

Merged
merged 38 commits into from
Jul 24, 2019
Merged

Conversation

sentialx
Copy link
Contributor

Enable full adblocking in Electron with only few lines of code:

import { ElectronBlocker } from '@cliqz/adblocker';
import { session } from 'electron';

// Create blocker instance
const blocker = ElectronBlocker.parse('||tracker.com'); // replace with desired subscriptions
await blocker.enableBlockingInSession(session.defaultSession);

Copy link
Collaborator

@remusao remusao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for contributing! As far as I can see the changes look great but ideally we would find a way to get type definitions for Electron without having to bundle the full package. Do you know if that is possible? Otherwise we could define these in the source code directly and make sure they are compatible with electron upstream via the example/electron?

src/request.ts Outdated Show resolved Hide resolved
src/electron/blocker.ts Outdated Show resolved Hide resolved
src/electron/blocker.ts Outdated Show resolved Hide resolved
@sentialx
Copy link
Contributor Author

sentialx commented Jun 27, 2019

Electron has its own type definitions and @types/electron package is outdated. I think we can add Electron to rollup external

@remusao
Copy link
Collaborator

remusao commented Jun 27, 2019

I'm wondering if this could cause issues for people using the adblocker without having electron installed. Since it's a dev dependency but it's still required from the bundle. Also not sure if the type definitions will be re-exported by TypeScript.

@sentialx
Copy link
Contributor Author

I implemented some cosmetic filtering, but it doesn't inject in all frames. I think you will need to check it.

@sentialx
Copy link
Contributor Author

I have no idea how to separate src/electron/content.ts to a dist/content.js file.

Copy link
Collaborator

@remusao remusao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few thoughts, wondering if in the future it would not make sense to have sub-packages for different environments (maybe managed with lerna, i see it's a pretty common practice): @cliqz/adblocker, @cliqz/adblocker-webextension, @cliqz/adblocker-puppeteer, @cliqz/adblocker-electron; which would give a bit more freedom with dependencies. At the moment it was kind of fine since we could get only the types for different platforms but Electron seem to require a tighter integration.

src/electron/content.ts Outdated Show resolved Hide resolved
src/electron/blocker.ts Outdated Show resolved Hide resolved
@remusao
Copy link
Collaborator

remusao commented Jul 3, 2019

@sentialx the mono-repo PR was merged so we should be able to move forward with the electron wrapper. I cannot guarantee that it will be 100% smooth as I'm still figuring-out the best workflow but I'm happy to improve the tooling/flow as we go. What do you think?

@sentialx
Copy link
Contributor Author

@remusao Sorry for the delay. Is there a way to separate packages/adblocker-electron/content.ts in rollup to build a separate js file?

@remusao
Copy link
Collaborator

remusao commented Jul 18, 2019

@remusao Sorry for the delay. Is there a way to separate packages/adblocker-electron/content.ts in rollup to build a separate js file?

No worries at all! Sure you can, if your rollup exports an array, you can define several configs (with different inputs and outputs). One such example would be: https://github.com/cliqz-oss/adblocker/blob/master/packages/adblocker/rollup.config.ts (another package in this repository); this way you could create a bundle for content and another for main process.

I noticed that you added by mistake the dist and build folders, could you maybe remove them, as it makes it harder to review the actual changes.

Copy link
Collaborator

@remusao remusao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also be able to remove package-lock.json completely. Since the switch to mono-repo structure yarn is needed to get access to the workspace feature it provides. So the only lock file needed is yarn.lock at the root of the repository.

rollup.config.ts Outdated Show resolved Hide resolved
@sentialx
Copy link
Contributor Author

The preload script is being injected before the page load, so in main process it's correct. However I'm not sure about the MutationObserver and messaging.

@remusao
Copy link
Collaborator

remusao commented Jul 22, 2019

The preload script is being injected before the page load, so in main process it's correct. However I'm not sure about the MutationObserver and messaging.

Makes sense. Is there a way to wait for DOM to be ready for injection maybe? Also, do you know if executeJavaScript and insertCSS can be called from renderer or main process interchangeably? Or will they take effect in different contexts?

@sentialx
Copy link
Contributor Author

Is there a way to wait for DOM to be ready for injection maybe?

In webContents there's a dom-ready event.

Also, do you know if executeJavaScript and insertCSS can be called from renderer or main process interchangeably?

In renderer process you can call webFrame.insertCSS and webFrame.executeJavaScript.
In main process the only difference is that you need webContents instead of a webFrame.

@remusao
Copy link
Collaborator

remusao commented Jul 23, 2019

@sentialx I've been investigating a bit further and gained some extra insight. It seems that when a new frame/content context is created and a page loads in it, sending a message to early might result in this message being dropped completely. Basically if:

  1. new URL is loaded in window
  2. preload script is injected and starts to run
  3. preload script sends a message to main process to request cosmetics
  4. main process replies with cosmetics (this is dropped most of the time because it happens to fast)

So we might need to way "a bit". Now waiting for dom-ready (or DOMContentLoaded, I think it's the same event right?) will be too late in some cases: ideally we want to start injecting as soon as possible (as long as the content context is ready to receive messages).

So now I'm not sure if we should just introduce an arbitrary delay before answering from main process, or if we could perform a preliminary handshake (at the cost of increased latency); like:

  1. preload script sends a message to main process
  2. main process sends the same message to content every N milliseconds, until it receives a reply
  3. preload script sends acknowledgement to main process whenever it receives the first message to signal it's fully operational.

What do you think? I will play a bit more with this tomorrow.

@sentialx
Copy link
Contributor Author

How is this possible, the renderer process loses the message sent from main process by request? I think it's something wrong with getting the styles to inject.

@remusao
Copy link
Collaborator

remusao commented Jul 23, 2019

How is this possible, the renderer process loses the message sent from main process by request? I think it's something wrong with getting the styles to inject.

What I did is this:

  1. console log when main process calls e.sender.send(...) (or e.reply(...))
  2. console log all messages received by renderer from main process

What I observed after starting the example multiple times:

  • the first message (sent before DOMContentLoaded) is received only part of the time
  • often, it is not received at all in renderer (but the sending is performed in main process)
  • subsequent messages from main to renderer seem to all be received.

Which is why I concluded that it might be due to a timing issue. Not sure if setting-up the ipc communication between processes might take some time?

@sentialx sentialx marked this pull request as ready for review July 23, 2019 20:29
Copy link
Collaborator

@remusao remusao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @sentialx! It's getting really close, very exciting. I had a few comments while reviewing the changes again. Let me know what you think.

packages/adblocker-electron/adblocker.ts Outdated Show resolved Hide resolved
packages/adblocker-electron/adblocker.ts Outdated Show resolved Hide resolved
packages/adblocker-electron/adblocker.ts Outdated Show resolved Hide resolved
packages/adblocker-electron/adblocker.ts Outdated Show resolved Hide resolved
packages/adblocker-electron/content.ts Outdated Show resolved Hide resolved
packages/adblocker-electron/content.ts Show resolved Hide resolved
packages/adblocker-electron/content.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

@remusao remusao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks a lot again! Looks very good. I'm going to give it a run on a list of websites where I know there are tricky cases for the adblocker to make sure everything works as expected (CSP headers, injections, etc.). Will merge after that.

private onGetCosmeticFilters = (
event: Electron.IpcMessageEvent,
url: string,
msg: IBackgroundCallback & { action?: string },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove & { action?: string }

}

private injectStyles(sender: Electron.WebContents, styles: string): void {
if (sender) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which case can sender be falsy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's a safe check whether it's not null or undefined.

@remusao
Copy link
Collaborator

remusao commented Jul 24, 2019

Looks great. I tried it a bit locally and things seem to work fine :D (just had to fix something related to CSP injection).

@remusao remusao merged commit a024239 into ghostery:master Jul 24, 2019
@remusao
Copy link
Collaborator

remusao commented Jul 24, 2019

⚡ 🚀 Nice work! It was a great experience and I learned a lot thanks to you. Thanks again for your hard work. I'm going to prepare a release soon so that we can start using this new abstraction in the wild.

@sentialx
Copy link
Contributor Author

Thanks. Will it be a good idea to add a blocked-ad event to help counting blocked ads?

@remusao
Copy link
Collaborator

remusao commented Jul 24, 2019

Thanks. Will it be a good idea to add a blocked-ad event to help counting blocked ads?

Yes, actually I was thinking that we could have a common mechanism to WebExtensionBlocker, PuppeteerBlocker and ElectronBlocker; to avoid repetition. It could even be implemented in @cliqz/adblocker (core package) in the FiltersEngine class so that all sub-classes benefit from this. I was not 100% sure about the API, and how much information we want to expose. There are multiple events that we could want to keep track of:

  • requests blocked
  • requests redirected
  • requests which could have been blocked but benefited from an exception
  • CSP headers injections
  • events related to cosmetics injections: scripts injected, styles injected

Depending on the project and use-case, you might want these information or not. Maybe a simple counter of "requests altered" is enough. But before implementing such mechanism, I'd like to answer these questions so that we build something solid which can accommodate multiple use-cases.

In terms of implementation details, it could be using some EventEmitter (maybe FiltersEngine could implement this API and define events for each case we are interested in), or using some callbacks. I'd also like to make sure the performance over-head is as small as possible. Ideally, the overhead is zero is no-one is listening to events, and negligible when events are consumed.

Let's continue the discussion in a separate issue. I'd love to get your thoughts about this.

@sentialx sentialx deleted the electron branch July 24, 2019 09:56
@remusao remusao mentioned this pull request Jul 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants