How can I alter the HTTP response body in a Firefox extension? I have setup an http-on-examine-response observer and an nsIStreamListener object with the code below. After I get the data, parse it, and alter it, how do I push the altered response back to the firefox browser? For example, let's say I go to Google.com with my extension enabled, the extension should intercept the response and change every occurence of "google" to "goggle". So when the page is loaded, the user will see "goggle" everywhere.
function TmSteroidsObserver()
{
this.register();
}
TmSteroidsObserver.prototype = {
observe: function(subject, topic, data) {
if (topic == "http-on-examine-response") {
}
else if (topic == "http-on-modify-request") {
var channel = subject.QueryInterface(Components.interfaces.nsIChannel);
var listener = new StreamListener(channel);
}
},
register: function() {
var observerService = Components.classes["#mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
observerService.addObserver(listener, "http-on-modify-request", false);
observerService.addObserver(listener, "http-on-examine-response", false);
},
unregister: function() {
var observerService = Components.classes["#mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
observerService.removeObserver(this, "http-on-modify-request");
observerService.removeObserver(this, "http-on-examine-response");
},
QueryInterface : function(aIID) {
if (aIID.equals(Components.interfaces.nsISupports) ||
aIID.equals(Components.interfaces.nsIObserver))
return this;
throw Components.results.NS_NOINTERFACE;
}
}
function StreamListener(channel) {
channel.notificationCallbacks = listener;
channel.asyncOpen(listener, null);
}
StreamListener.prototype = {
mData: "",
mChannel: null,
// nsIStreamListener
onStartRequest: function (aRequest, aContext) {
this.mData = "";
},
onDataAvailable: function (aRequest, aContext, aStream, aSourceOffset, aLength) {
var scriptableInputStream =
Components.classes["#mozilla.org/scriptableinputstream;1"]
.createInstance(Components.interfaces.nsIScriptableInputStream);
scriptableInputStream.init(aStream);
this.mData += scriptableInputStream.read(aLength);
},
onStopRequest: function (aRequest, aContext, aStatus) {
if (Components.isSuccessCode(aStatus)) {
// request was successfull
this.mCallbackFunc(this.mData);
} else {
// request failed
this.mCallbackFunc(null);
}
this.mChannel = null;
},
// nsIChannelEventSink
onChannelRedirect: function (aOldChannel, aNewChannel, aFlags) {
// if redirecting, store the new channel
this.mChannel = aNewChannel;
},
// nsIInterfaceRequestor
getInterface: function (aIID) {
try {
return this.QueryInterface(aIID);
} catch (e) {
throw Components.results.NS_NOINTERFACE;
}
},
// nsIProgressEventSink (not implementing will cause annoying exceptions)
onProgress : function (aRequest, aContext, aProgress, aProgressMax) { },
onStatus : function (aRequest, aContext, aStatus, aStatusArg) { },
// nsIHttpEventSink (not implementing will cause annoying exceptions)
onRedirect : function (aOldChannel, aNewChannel) { },
// we are faking an XPCOM interface, so we need to implement QI
QueryInterface : function(aIID) {
if (aIID.equals(Components.interfaces.nsISupports) ||
aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
aIID.equals(Components.interfaces.nsIChannelEventSink) ||
aIID.equals(Components.interfaces.nsIProgressEventSink) ||
aIID.equals(Components.interfaces.nsIHttpEventSink) ||
aIID.equals(Components.interfaces.nsIStreamListener))
return this;
throw Components.results.NS_NOINTERFACE;
}
};
You can use nsITraceableChannel to intercept the response.
You should modify the data which is available to what you need and pass it to the innerListener's OnDataAvailable
Below links would help you understand this better.
http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
http://www.ashita.org/howto-xhr-listening-by-a-firefox-addon/
For future readers looking for a way to do this in Firefox Quantum, there is an API that lets you filter responses. Using the method for long documents mentioned here, I was able to reliably change what I needed in my (temporary) plugin's background.js like so:
browser.webRequest.onBeforeRequest.addListener(
function fixenator(details) {
let filter = browser.webRequest.filterResponseData(details.requestId);
let decoder = new TextDecoder("utf-8");
let encoder = new TextEncoder();
let str = '';
filter.ondata = event => {
str += decoder.decode(event.data, {stream: true});
};
filter.onstop = event => {
str = str.replace(/searchPattern/g, 'replace pattern');
filter.write(encoder.encode(str));
filter.close();
}
return {};
},
{
urls: ['https://example.com/path/to/url']
//, types: ['main_frame', 'script', 'sub_frame', 'xmlhttprequest', 'other'] // optional
}
, ['blocking']
);
The observer service just call your listeners. Firefox will receive the requests,call your listeners, and send responses. see Mozilla docs Creating HTTP POSTs.
Related
I want to draw 5 graph a single page based on different condition. Actually I am using a loop and send different data each time and based on that condition I want to create a graph. But don't know why the library d3.js is only trigger the draw function in the end and create a single graph based on last condition. also I am using apache wicket framework
I call the function 5 time using a loop and send different data
final def rawData(id:String): String = {
s"""
require(["renderer"], function (renderer) {
renderer.init("#${id}");
var data = 'digraph g {\\n ${data.attributes.trim} \\n' +
${if (nodes.isEmpty) "''" else nodes} +
${if (links.isEmpty) "''" else links} +
'}';
const getData = async() => {
await renderer.render(data);
}
getData();
});
"""
}
Upper function trigger the below function but in below function init function is call each time and set the data in javascript . Also render function is also call each time but it is not going to ready stage each time it is going in pending and in last it trigger the ready stage and draw the last graph only.
define('renderer',["stage", "worker!layout-worker.js"], function(stage, worker) {
var initialized = false, pending, errorCallback, renderCallback;
var bool =false;
worker.onmessage = function (event) {
switch (event.data.type) {
case "ready":
initialized = true;
if (pending) {
worker.postMessage(pending);
}
break;
case "stage":
stage.draw(event.data.body);
console.log("stage =================> ",event.data.type);
renderCallback && renderCallback();
break;
case "error":
if (errorCallback) {
errorCallback(event.data.body);
}
}
};
return {
init: function(element) {
return stage.init(element);
},
render: function(source) {
if (initialized) {
worker.postMessage(source);
} else {
pending = source;
}
},
stage: stage,
errorHandler: function(handler) {
errorCallback = handler;
},
renderHandler: function(handler) {
renderCallback = handler;
}
};
});
require(/*{
baseUrl: "."
},*/
["transformer"],
function(transformer) {
onmessage = function(event) {
try {
var result = transformer.generate(event.data);
postMessage({
type: "stage",
body: result
});
} catch (e) {
postMessage({
type: "error",
body: e
});
}
};
postMessage({
type: "ready"
});
}
);
I've build a JS plugin that I'm loading into my project. I'm using Gulp 4 to compile everything, and I'm struggling with one of my methods. The method accepts a boolean value of true or false, and if the value of true is specified, then the function that the method calls runs an API request which updates my plugin's options.
However, in the project that listens for the response of the method, it doesn't account for the response of the API request.
Plugin:
(function() {
this.MyPlugin = function() {
/*
** Settings
**
** Default settings of the plugin that we can overwrite later
*/
const INBOUND_CONFIG = {
features: {
settingOne: 'setting one',
settingTwo: 'setting two'
}
}
// Create options by extending defaults with the passed in arugments
if (arguments[0] && typeof arguments[0] === "object") {
this.options = extendDefaults(INBOUND_CONFIG, arguments[0]);
}
/*
** Compile Detailed Settings
**
** Compile detailed settings
*/
function compactFeatures (config) {
const tlp_aff_id = buildTlpAffID(
config.features.cpm_id,
config.features.sub_id,
config.brand
)
return {
tlp_aff_id: tlp_aff_id
}
}
/*
** Get inbound options
**
** Get inbound options if we're not on the inbound page so that they're,
** accessible throughout the app
*/
function getSettings (prefersDB = false) {
purgeInboundConfig(false)
let inbound
if (localStorage.getItem('example--inbound')) {
inbound = JSON.parse(encodeAndDecode(localStorage.getItem('example--inbound'), 'decode'))
} else {
inbound = JSON.parse(localStorage.getItem('example--inbound'))
}
// prefers db
if (prefersDB) {
fetchFromDB()
return
}
let features = { ...compare(inbound, INBOUND_CONFIG.features), ...compactFeatures(INBOUND_CONFIG) }
INBOUND_CONFIG.features = validateInboundSettings(features)
}
/*
** Comparison
**
** We need to compare the inbound settings parsed against the default,
** settings that we have. If one from the inbound settings matches a,
** default one, we overwrite it and use the value from the inbound
*/
function compare (inbound, defaults) {
let settings = defaults
if (!inbound) return settings
for (const [key, value] of Object.entries(inbound)) {
for (var option in defaults) {
if (defaults.hasOwnProperty(option)) {
if (key == option) {
settings[key] = convertToOriginalType(value)
continue
}
}
}
}
return settings
}
/*
** Find Affiliate from DB
**
** Find an affiliate from the DB and fetch the appropriate data
**
*/
function fetchFromDB () {
const query = JSON.parse(encodeAndDecode(localStorage.getItem('example--query'), 'decode'))
// construct the data that we need
const data = {
affiliate: query.cpm_id ?? '',
query: query ?? []
}
makeInboundHTTPRequest('POST', `https://example.com/api/affiliate/find`, 5000, JSON.stringify(data), function () {
const res = JSON.parse(this.response.data.response)
INBOUND_CONFIG.features = res.options ?? []
// rerun to get settings
getSettings()
})
}
/*
** HTTP requests
**
** This function will handle HTTP requests made by the plugin to and from,
** the processor.
*/
function makeInboundHTTPRequest (
type = 'GET',
url = '',
timeout = 30000,
payload = null,
callback
) {
const xhr = new XMLHttpRequest()
xhr.open(type, url, true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.timeout = timeout
xhr.ontimeout = () => {
callback.apply({
response: { error: true, message: `Timeout exceeded ${timeout}ms`, data: null }
})
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.response != null) {
callback.apply({
response: { error: false, message: 'Success', data: xhr }
})
} else {
callback.apply({
response: { error: false, message: 'No data', data: null }
})
}
}
}
xhr.onerror = () => {
callback.apply({
response: { error: true, message: 'Generic error', data: null }
})
}
if (type === 'POST') {
xhr.send(payload)
return
}
xhr.send()
}
/*
** Get Inbound Options
**
** Get Inbound Config
*/
MyPlugin.prototype.getSettings = function(prefersDB = false) {
getSettings(prefersDB)
return INBOUND_CONFIG.features
}
/*
** Extend Defaults
**
** Utility method to extend defaults with user options
*/
function extendDefaults(source, properties) {
var property;
for (property in properties) {
if (properties.hasOwnProperty(property)) {
source[property] = properties[property];
}
}
return source;
}
}
/*
** Plugin Loaded
**
** Let the client know that the plugin is working!
*/
window.postMessage('example--inbound-ready', '*')
}());
The above code is incomplete and doesn't contain every function, but purely contains the related one to my question which is:
MyPlugin.prototype.getSettings = function(prefersDB = false) {
getSettings(prefersDB)
return INBOUND_CONFIG.features
}
Now, if I add a setTimeout to this method, then INBOUND_CONFIG.features has the correct returned response when the value of true is passed to the function, otherwise it doesn't
Site
/*
** Define our plugins
**
** Define our plugins that we want to enable
*/
var inbound
const plugins = {
inbound: {
isEnabled: false
}
}
/*
** Enable Plugins
**
** Listen for plugins, if they're included then let's enable them
*/
const eventMethod = window.addEventListener ? 'addEventListener' : 'attachEvent'
const eventer = window[eventMethod]
const messageEvent = eventMethod == 'attachEvent' ? 'onmessage' : 'message'
// Listen to message from child window
eventer(messageEvent, function(e) { // eslint-disable-line prefer-arrow-callback
const str = e.data.toString()
if (str == 'myplugin--inbound-ready') {
// Enable the plugin
plugins.inbound.isEnabled = true
// Load Inbound Plugin
if (plugins.inbound.isEnabled) {
mypluginInbound = new MyPlugin()
inbound = mypluginInbound.getSettings(true) <-- this doesn't contain updated api settings
} else {
inbound = ''
}
}
}, false)
How can I make my getSettings method wait for a response, do I add a set timeout to it, or a promise? I can't use async/await in this project for bigger reasons.
I work over a small React app that should store some data in local storage. And if I understand it correctly, it should keep them till clearly ordered to clear. The reason for this post is that while app data perfectly survives the refreshing page action, it disappears after closing/opening Chrome.
Here is the function which creates the storage:
export function initGlobalStorage() {
var Storage = function (options) {
options = options || {};
for (var i in options) {
this[i] = options[i];
}
};
const prefix = window.location.href;
Storage.prototype = {
storage: window.localStorage,
addPrefix: function(key){return (prefix + key); },
set: function (key, value) {
this.storage.setItem(this.addPrefix(key), JSON.stringify(value));
},
get: function (key) {
return this.storage.getItem(this.addPrefix(key));
},
remove: function (key, value) {
this.storage.remove(key, value);
},
clear: function () {
this.storage.clear();
},
key: function (index) {
return this.storage.key(index);
},
each: function (fn) {
if (typeof fn === "function") {
for (var i = 0, key; i < this.storage.length; ++i) {
key = this.storage.key(i);
fn(this.storage.getItem(key), key, i);
}
}
},
getAll:function(){
let result =[];
for(var key in this.storage){
if (key.includes(prefix)){result.push(JSON.parse(this.storage.getItem(key)))};
}
return result;
},
hasItems:function(){
console.log(this.getAll().length);
return this.getAll().length? true:false;
},
};
window.Storage = {
local: new Storage({ storage: window.localStorage }),
session: new Storage({ storage: window.sessionStorage }),
};
};
window.local.href is for distinguish 'my items' in localStorage from others that possibly exists there on client computer. BTW, currently I only test this app on localhost.
Here is how above function is applied
export function checkSupportForCache() {
return dispatch => {
if (storageAvailable('localStorage')) {
dispatch(cacheSupported());
console.warn("Storage available");
initGlobalStorage();
if (window.Storage.local.hasItems()){
console.log('Storage contains items');
dispatch(cacheNotEmpty());
}else{
console.warn("No items in storage");
}
} else {
console.warn("Storage not available");
}
};
}
storageAvailable('localStorage') is a function that checks support for localStorage in a certain browser.
As I have written, when I refresh page localStorage is still there - then I suppose code is OK.
But what happens when do close browser then? I do not consciously request for any sort of purging action. Do I unconsciously? I have checked Chrome settings and there is nothing that looks suspicious. Do I not understand anything at all as per subject? Maybe, just give me a hint.
Ive been trying to get this code working for firefox, that I got from
nsITraceableChannel, Intercept HTTP Traffic and from How to get an url from nsITraceableChannel?, Ive been googling for the answer, with no luck.
What I would like to do is intercept certain links, and change them.
Any help is welcomed
The error I get is function statement requires a name witch points to observe: function(aSubject, aTopic, aData) part of the code
const Cc = Components.classes;
const Ci = Components.interfaces;
var observerService = Cc["#mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
observerService.addObserver(httpRequestObserver,
"http-on-examine-response", false);
observerService.removeObserver(httpRequestObserver,
"http-on-examine-response");
//--------------------------------------------------------
var httpRequestObserver =
{
observe: function(aSubject, aTopic, aData)
{
if (aTopic == "http-on-examine-response")
{
}
},
QueryInterface : function (aIID)
{
if (aIID.equals(Ci.nsIObserver) ||
aIID.equals(Ci.nsISupports))
{
return this;
}
throw Components.results.NS_NOINTERFACE;
}
}
//------------------------------------------------------------
function TracingListener() {
this.originalListener = null;
}
TracingListener.prototype =
{
onDataAvailable: function(request, context, inputStream, offset, count) {
this.originalListener.onDataAvailable(request, context, inputStream, offset, count);
},
onStartRequest: function(request, context) {
this.originalListener.onStartRequest(request, context);
},
onStopRequest: function(request, context, statusCode) {
this.originalListener.onStopRequest(request, context, statusCode);
},
QueryInterface: function (aIID) {
if (aIID.equals(Ci.nsIStreamListener) ||
aIID.equals(Ci.nsISupports)) {
return this;
}
throw Components.results.NS_NOINTERFACE;
}
}
observe: function(aSubject, aTopic, aData)
{
if (aTopic == "http-on-examine-response") {
var newListener = new TracingListener();
aSubject.QueryInterface(Ci.nsITraceableChannel);
newListener.originalListener = aSubject.setNewListener(newListener);
}
}
aSubject = aSubject.QueryInterface(Ci.nsIChannel);
var uri = aSubject.URI;
// original URI that was requested before any other resolver steps
// and/or redirects.
var ouri = aSubject.originalURI;
ch = Services.io.newChannel("https://google.com/", null, null);
console.log(ch.toString());
// "[xpconnect wrapped nsIChannel]"
ch.QueryInterface(Ci.nsITraceableChannel);
console.log(ch.toString());
// "[xpconnect wrapped (nsISupports, nsIChannel, nsITraceableChannel)]"
console.log(ch instanceof Ci.nsIUploadChannel);
// true
console.log(ch.toString());
// "[xpconnect wrapped (nsISupports, nsIChannel, nsITraceableChannel, nsIUploadChannel)]"
// the variable "ch" is known to implement the four given interfaces at this point.
Copy paste this it works:
// const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
// Cu.import('resource://gre/modules/Services.jsm');
Services.obs.addObserver(httpRequestObserver, "http-on-examine-response", false);
// Services.obs.removeObserver(httpRequestObserver, "http-on-examine-response");
//--------------------------------------------------------
var httpRequestObserver = {
observe: function(aSubject, aTopic, aData) {
// if (aTopic == "http-on-examine-response") {} // no need for this, we added observer for `http-on-examine-response` so we know for sure this only triggers for `http-on-examine-response`
var newListener = new TracingListener();
aSubject.QueryInterface(Ci.nsITraceableChannel);
newListener.originalListener = aSubject.setNewListener(newListener);
}
/* // what was the point of this block of code?
,
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsIObserver) || aIID.equals(Ci.nsISupports)) {
return this;
}
throw Cr.NS_NOINTERFACE;
}
*/
}
//------------------------------------------------------------
function TracingListener() {}
TracingListener.prototype = {
onDataAvailable: function(request, context, inputStream, offset, count) {
console.log('data available');
this.originalListener.onDataAvailable(request, context, inputStream, offset, count);
},
onStartRequest: function(request, context) {
this.originalListener.onStartRequest(request, context);
},
onStopRequest: function(request, context, statusCode) {
this.originalListener.onStopRequest(request, context, statusCode);
},
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsIStreamListener) || aIID.equals(Ci.nsISupports)) {
return this;
}
throw Cr.NS_NOINTERFACE;
}
}
// what the hell? this is not part of an object? why observe colon?
/*
observe: function(aSubject, aTopic, aData) {
if (aTopic == "http-on-examine-response") {
var newListener = new TracingListener();
aSubject.QueryInterface(Ci.nsITraceableChannel);
newListener.originalListener = aSubject.setNewListener(newListener);
}
}
*/
// whats with all this junk? is it supposed to be in an observer??
/*
aSubject = aSubject.QueryInterface(Ci.nsIChannel);
var uri = aSubject.URI;
// original URI that was requested before any other resolver steps
// and/or redirects.
var ouri = aSubject.originalURI;
ch = Services.io.newChannel("https://google.com/", null, null);
console.log(ch.toString());
// "[xpconnect wrapped nsIChannel]"
ch.QueryInterface(Ci.nsITraceableChannel);
console.log(ch.toString());
// "[xpconnect wrapped (nsISupports, nsIChannel, nsITraceableChannel)]"
console.log(ch instanceof Ci.nsIUploadChannel);
// true
console.log(ch.toString());
// "[xpconnect wrapped (nsISupports, nsIChannel, nsITraceableChannel, nsIUploadChannel)]"
// the variable "ch" is known to implement the four given interfaces at this point.
*/
Update
Someone asked me how to get the response source with this, so I created a gist out of it: https://gist.github.com/Noitidart/d0b08629ee2804538ad9#file-_ff-addon-snippet-copyofrequestsource-js-L16
I have a meteor app that allows users to update their skype name, phone number, email address, etc. To help maintain a consistent code base I have implemented an EJSON type UserModel in a common directory so it can run on the client and server.
EJSON.addType("UserModel", function fromJSONValue(value) {
return new UserModel(value);
});
UserModel.prototype = {
constructor: UserModel,
//
// EJSON Ovverrides.
//
valueOf: function() {
return JSON.parse(JSON.stringify(this), function(key, value) {
var dateFields = ["expiration", "createdAt"];
if(_.contains(dateFields, key) && typeof value === "string") {
return new Date(value);
} else {
return value;
}
});
},
typeName: function() {
return 'UserModel';
},
toJSONValue: function() {
return this.valueOf();
},
clone: function() {
return new UserModel(this.valueOf());
},
equals: function(other) {
if(!(other instanceof UserModel)) {
return false;
}
return this._id === other._id;
},
setPhoneNumbers: function(phoneNumber, queueUpdate) {
var modifier = {$set: {
'profile.phoneNumber': phoneNumber
}};
this.profile.phoneNumber = phoneNumber;
return this._saveOrQueueUpdate(modifier, queueUpdate);
},
_saveOrQueueUpdate: function(modifier, queueUpdate) {
if (!queueUpdate) {
return Meteor.users.update(this._id, modifier, function(err, res) {
});
} else {
this.pendingUpdates.push(modifier);
return true;
}
}
I call the setPhoneNumbers method on the settings page js file like so.
'blur #phonenumber':function(){
var user = Meteor.user();
var number = $("#phonenumber").val();
if(number.length){
user.setPhoneNumbers(number);
}
}
The problem with this is that whenever I call the setPhoneNumbers method, the page takes >500ms to update and locks the entire page. I looked at the docs and according to this segment, client code should never be blocking. The page only locks up when updates happen so I know it has something to do with the UserModel. Any insight to what could be causing this would be very helpful. The page is extremely slow and it is unacceptable for a production app.