Skip to content

Commit

Permalink
Merge pull request #32 from brianvanburken/improvements-catching-images
Browse files Browse the repository at this point in the history
Improvements catching images
  • Loading branch information
ayastreb committed Oct 4, 2019
2 parents b0acd4e + bccd6fb commit aa8536e
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 176 deletions.
275 changes: 149 additions & 126 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,125 +5,130 @@ import parseUrl from './utils/parseUrl'
import deferredStateStorage from './utils/deferredStateStorage'
import defaultState from './defaults'
import axios from 'axios'
import isPrivateNetwork from './background/isPrivateNetwork';

chrome.storage.local.get(storedState => {
const storage = deferredStateStorage()
let setupOpen
let state
let pageUrl
let compressed = new Set();
let isWebpSupported
const compressed = new Set();
let setupHasBeenOpened = false;
let state = { ...defaultState, ...storedState };
let currentPageUrl = null;
let currentPageProtocol = null;

if (/compressor\.bandwidth-hero\.com/i.test(storedState.proxyUrl)) {
chrome.storage.local.set({ ...storedState, proxyUrl: '' })
}

setState({ ...defaultState, ...storedState })

checkWebpSupport().then(isSupported => {
isWebpSupported = isSupported
chrome.storage.local.set({ ...storedState, isWebpSupported: isSupported })
})

async function checkWebpSupport() {
if (!self.createImageBitmap) return false

const webpData = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='
const blob = await fetch(webpData).then(r => r.blob())
return self.createImageBitmap(blob).then(() => true, () => false)
}

/**
* Sync state.
*/
function setState(newState) {
if (!state || state.enabled !== newState.enabled) {
setIcon(newState.enabled);
}
state = newState
//attach or remove listeners based on state.enabled
state.enabled ? attachListeners() : detachListeners()
return true //acknowledge
const webpData = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='
const blob = await fetch(webpData).then(r => r.blob())
return self.createImageBitmap(blob).then(() => true, () => false)
}

/**
* Sets the icons based on the disabled parameter.
* @param enabled
*/
function setIcon(enabled) {
function setIcon() {
const isEnabled = state.enabled && !isDisabledSite();
if (chrome.browserAction.setIcon) {
chrome.browserAction.setIcon({
path: enabled ? 'assets/icon-128.png' : 'assets/icon-128-disabled.png'
})
path: isEnabled ? "assets/icon-128.png" : "assets/icon-128-disabled.png"
});
}
}

/**
* Checks if the proxy is disabled for the given url
* @param url
* @returns {boolean}
*/
function isDisabledSite(url) {
const disabledHosts = state && state.disabledHosts || []
return disabledHosts.includes(url)
function isDisabledSite() {
// If we don't have the URL or protocol we can't check if it is enabled.
if (!currentPageUrl || !currentPageProtocol) {
return true;
}

// Check if the page is a http/https page.
const supportedProtocols = ['http:', 'https:'];
if (!supportedProtocols.includes(currentPageProtocol)) {
return true;
}

// We are disabled when on localhost or site is hosted on private IP.
if (isPrivateNetwork(currentPageUrl)) {
return true;
}
return state.disabledHosts.includes(currentPageUrl);
}

/**
* refreshState
* Every time the storage of the browser changes we also update our in-memory state to keep up with the changes.
*
*/
function updateState(changes) {
const changedItems = Object.keys(changes)
function onStateChanged(changes) {
const changedItems = Object.keys(changes);
for (const item of changedItems) {
if( state[item] !== changes[item].newValue){
state[item] = changes[item].newValue
if(item === "enabled"){
state.enabled ? attachListeners() : detachListeners()
setIcon(state.enabled);
} else if (item === "disabledHosts") {
const disabled = isDisabledSite(pageUrl)
setIcon(!disabled);
}
if (state[item] !== changes[item].newValue) {
state[item] = changes[item].newValue;
stateItemChanged(item, state[item]);
}
}
}

function checkSetup() {
if(state.enabled){
attachListeners()
if (
!setupOpen &&
(state.proxyUrl === '' || /compressor\.bandwidth-hero\.com/i.test(state.proxyUrl))
) {
chrome.tabs.create({ url: 'setup.html' })
setupOpen = true
/**
* Perform actions for certain changes to the state. Like changing the icon if we enable/disable the extension.
* @param key
* @param newValue
*/
function stateItemChanged(key, newValue) {
switch (key) {
case 'enabled':
case 'disabledHosts':
setIcon(); // Update icon
break;
}
}

}
function checkSetup() {
if (!state.enabled) return;
if (setupHasBeenOpened) return;
if (state.proxyUrl === '' || /compressor\.bandwidth-hero\.com/i.test(state.proxyUrl)) {
chrome.tabs.create({ url: 'setup.html' });
setupHasBeenOpened = true;
}
}

function onInstalled() {
checkSetup();
}

/**
* Intercept image loading request and decide if we need to compress it.
*/
function onBeforeRequestListener({ url, documentUrl, type }) {
checkSetup()
checkSetup();

// Occasionally currentPageUrl is not ready in time on FF
const pageUrl = currentPageUrl || parseUrl(documentUrl).host;

if (
shouldCompress({
imageUrl: url,
pageUrl: pageUrl || parseUrl(documentUrl).host, //occasionally pageUrl is not ready in time on FF
compressed,
proxyUrl: state.proxyUrl,
disabledHosts: state.disabledHosts,
enabled: state.enabled,
type
pageUrl,
compressed,
proxyUrl: state.proxyUrl,
disabledHosts: state.disabledHosts,
enabled: state.enabled,
type
})
) {
compressed.add(url)
let redirectUrl = `${state.proxyUrl}?url=${encodeURIComponent(url)}`
if (!isWebpSupported) redirectUrl += '&jpeg=1'
if (!state.convertBw) redirectUrl += '&bw=0'
if (state.compressionLevel) {
redirectUrl += '&l=' + parseInt(state.compressionLevel, 10)
}
const redirectUrl = buildCompressUrl(url);

if (!isFirefox()) return { redirectUrl }
// Firefox allows onBeforeRequest event listener to return a Promise
// and perform redirect when this Promise is resolved.
Expand All @@ -147,6 +152,26 @@ chrome.storage.local.get(storedState => {
}
}

/**
* Builds up a redirect URL. The original URL is encode and added as query
* parameter. If WebP isn't supported the url append a flag to let the
* server know to return in a JPEG format instead. Other flags that are
* added are: bw (this lets the proxy know to return image in grayscale
* as those have less color information and thus saving more space) and
* compression level.
* @param url
* @returns {string}
*/
function buildCompressUrl(url) {
let redirectUrl = '';
redirectUrl += state.proxyUrl;
redirectUrl += `?url=${encodeURIComponent(url)}`;
redirectUrl += `&jpeg=${state.isWebpSupported ? 0 : 1}`;
redirectUrl += `&bw=${state.convertBw ? 1 : 0}`;
redirectUrl += `&l=${state.compressionLevel}`;
return redirectUrl;
}

/**
* Firefox user agent always has "rv:" and "Gecko"
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox
Expand All @@ -160,9 +185,10 @@ chrome.storage.local.get(storedState => {
* app storage and notify UI about state changes.
*/
function onCompletedListener({ responseHeaders, fromCache }) {
if (fromCache) return;
const bytesSaved = getHeaderValue(responseHeaders, 'x-bytes-saved')
const bytesProcessed = getHeaderValue(responseHeaders, 'x-original-size')
if (bytesSaved !== false && bytesProcessed !== false && fromCache === false) {
if (bytesSaved !== false && bytesProcessed !== false) {
state.statistics.filesProcessed += 1
state.statistics.bytesProcessed += bytesProcessed
state.statistics.bytesSaved += bytesSaved
Expand All @@ -171,20 +197,21 @@ chrome.storage.local.get(storedState => {
}
}

function tabActivationListener({tabId}) {
function onTabActivated({tabId}) {
chrome.tabs.get(tabId, tab => {
pageUrl = parseUrl(tab.url).hostname;
const disabled = isDisabledSite(pageUrl)
setIcon(!disabled)
const url = parseUrl(tab.url);
currentPageUrl = url.hostname;
currentPageProtocol = url.schema;
compressed.clear(); // Reset our list of compressed images
setIcon();
});
}

function tabUpdateListener(){
if(compressed instanceof Set){
compressed.clear()
}else{
compressed = new Set()
}
// If we navigate to a new page within a tab and it is the same we have a
// bug where it does not process images. Because the images are still in
// compressed even though the page changed. With onTabUpdated we reset this.
function onTabUpdated(){
compressed.clear()
}

/**
Expand All @@ -196,57 +223,53 @@ chrome.storage.local.get(storedState => {
responseHeaders: patchContentSecurity(responseHeaders, state.proxyUrl)
}
}
function attachListeners(){
if(!chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestListener)){
chrome.webRequest.onBeforeRequest.addListener(
onBeforeRequestListener,
{
urls: ['<all_urls>'],
types: isFirefox() ? ['xmlhttprequest', 'imageset', 'image'] : ['image']
},
['blocking']
)
}
if(!chrome.webRequest.onCompleted.hasListener(onCompletedListener)){
chrome.webRequest.onCompleted.addListener(
onCompletedListener,
{
urls: ['<all_urls>'],
types: isFirefox() ? ['xmlhttprequest', 'imageset', 'image'] : ['image']
},
['responseHeaders']
)
}
if(!chrome.webRequest.onHeadersReceived.hasListener(onHeadersReceivedListener)){
chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceivedListener,
{
urls: ['<all_urls>'],
types: ['main_frame', 'sub_frame', 'xmlhttprequest']
},
['blocking', 'responseHeaders']
)
}
if(!chrome.tabs.onActivated.hasListener(tabActivationListener)){
chrome.tabs.onActivated.addListener(tabActivationListener)
}
if(!chrome.tabs.onUpdated.hasListener(tabUpdateListener)){
chrome.tabs.onUpdated.addListener(tabUpdateListener)
}

if (!chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestListener)) {
chrome.webRequest.onBeforeRequest.addListener(
onBeforeRequestListener,
{
urls: ['<all_urls>'],
types: isFirefox() ? ['xmlhttprequest', 'imageset', 'image'] : ['image']
},
['blocking']
)
}

if (!chrome.webRequest.onCompleted.hasListener(onCompletedListener)) {
chrome.webRequest.onCompleted.addListener(
onCompletedListener,
{
urls: ['<all_urls>'],
types: isFirefox() ? ['xmlhttprequest', 'imageset', 'image'] : ['image']
},
['responseHeaders']
)
}

if (!chrome.webRequest.onHeadersReceived.hasListener(onHeadersReceivedListener)) {
chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceivedListener,
{
urls: ['<all_urls>'],
types: ['main_frame', 'sub_frame', 'xmlhttprequest']
},
['blocking', 'responseHeaders']
)
}

function detachListeners(){
chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequestListener)
chrome.webRequest.onCompleted.removeListener(onCompletedListener)
chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceivedListener)
chrome.tabs.onActivated.removeListener(tabActivationListener)
chrome.tabs.onUpdated.removeListener(tabUpdateListener)
if (!chrome.tabs.onActivated.hasListener(onTabActivated)) {
chrome.tabs.onActivated.addListener(onTabActivated)
}

if(!chrome.storage.onChanged.hasListener(updateState)){
chrome.storage.onChanged.addListener(updateState)
if (!chrome.tabs.onUpdated.hasListener(onTabUpdated)) {
chrome.tabs.onUpdated.addListener(onTabUpdated)
}
if(!chrome.runtime.onInstalled.hasListener(checkSetup)){
chrome.runtime.onInstalled.addListener(checkSetup)

if (!chrome.storage.onChanged.hasListener(onStateChanged)) {
chrome.storage.onChanged.addListener(onStateChanged)
}

if (!chrome.runtime.onInstalled.hasListener(onInstalled)) {
chrome.runtime.onInstalled.addListener(onInstalled)
}
})
25 changes: 25 additions & 0 deletions src/background/isPrivateNetwork.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Netmask } from 'netmask'

/**
* Check if an URL is localhost or part of a private IP. If it is private we
* can't reach it with our proxy. So we should skip compressing those URLs.
*/
export default (url) => {
if (url === 'localhost' || /^https?:\/\/(localhost|127.0.0.1).*/i.test(url)) {
return true;
}

const ipAddress = url.match(/^https?:\/\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*/)
if (ipAddress) {
const privateBlocks = [
new Netmask('10.0.0.0/8'),
new Netmask('172.16.0.0/12'),
new Netmask('192.168.0.0/16')
]
for (const block of privateBlocks) {
if (block.contains(ipAddress[1])) return true
}
}

return false
}
Loading

0 comments on commit aa8536e

Please sign in to comment.