In this article, I continue the cycle of publications, in which I want to talk about my experience writing a web browser extension. I already had the experience of creating a web extension, which was installed by about 100,000 Chrome users who worked autonomously, but in this series of articles I decided to delve into the process of developing a web extension by tightly integrating it with the server part.




Part 1 ,
Part 2 ,
Part 4Pitfalls in the implementation of the interaction of the web extension and the server part
As already described earlier for the server part, Meteor.js is used. To implement the RESTful API, use the
github.com/kahmali/meteor-restivus package. He already has some implemented part to cover user mechanisms associated with authorization.
For example, it is sufficient to specify
authRequired: true , as in the example below, in order for the API point to work only for authorized users.
Api.addRoute('clientScript/:id_script', {authRequired: true}, {get: { action: function() {
Thus, three API points were added for registration, for receiving profile data and updating it, for resetting the password.
In the web extension itself, when calling methods that require authorization, the following code is used:
var details = { url: API_URL + '/api/v1/clientDataRowDownload/' + dataRowId + '/download', method: 'GET', contentType: 'json', headers: {'X-Auth-Token': kango.storage.getItem("authToken"), 'X-User-Id': kango.storage.getItem("userId")} }; kango.xhr.send(details, function(data) {
Here is a clear example of a request with authorization. In the headers, X-Auth-Token and X-User-Id are transmitted as a result of the registration or authorization process. This data is stored in the local storage of the web extension and is always available in the content.js script.
Downloading files in the web extension is done by reading the file on the browser side and sending it via XHR:
$("form#uploadFile").on("submit", function(event, template) { event.preventDefault(); var reader = new FileReader(); reader.onload = function(evt) { var details = { url: API_URL + '/api/v1/clientFileAdd/' + kango.storage.getItem("userId"), method: 'POST', contentType: 'json', params: {"content": encodeURIComponent(evt.target.result.replace(/^data:[^;]*;base64,/, "")), "name": encodeURIComponent(event.currentTarget.fileInput.files[0].name), "size": event.currentTarget.fileInput.files[0].size, "type": event.currentTarget.fileInput.files[0].type, "lastModified": event.currentTarget.fileInput.files[0].lastModified }, headers: {'X-Auth-Token': kango.storage.getItem("authToken"), 'X-User-Id': kango.storage.getItem("userId")} }; kango.xhr.send(details, function(data) { if (data.status == 200 && data.response != null) { if(data.response.status == "success") {
Here it is important to mark the line
event.target.result.replace (/ ^ data: [^;] *; base64, /, "") . The file on the browser side is encoded in base64, but for server-side compatibility when using this encoding in the line
Buffer.from (new String (this.bodyParams.content), “base64”) we have to cut off the encoding prefix and read only the “body” of the file . It is also necessary to note the wrapping in the encodeURIComponent, since the same
+ is often found in base64 and file names.
When editing scripts, you must consider the character encoding in the script body when transmitting content. In some cases, base64 encoding did not give the correct results when decoding on the server side when using encodeURIComponent. Therefore, prefetch encoding is used in utf8 with utf8.encode (str); where
mths.be/utf8js v3.0.0 from @mathias
File downloads are implemented using the well-proven FileSaver library. The data received via XHR is simply transferred to the input of the File constructor, and then the file download is initialized:
var file = new File([data.response.data.join("\n")], "data_rows" + date.getFullYear() + "_" + (date.getMonth() + 1) + "_" + date.getDate() + ".csv", {type: "application/vnd.ms-excel"}); saveAs(file);
Internal library for custom scripts
For interaction between the script, the web extension and the server part, it is necessary to have an intermediate link that allows you to quickly receive data from the downloaded file, save data after the script is executed, etc.
For this purpose, an internal library was written, which is initialized before any script starts working by adding itself to the page code. Here you need to add information about the security policy of sources to download resources, namely about the content-security-policy.
Many sites use CSP headers to protect against the execution of arbitrary javascript code on the pages of their web services, thus protecting themselves from XSS on the side of the web browser.
Since the user installs the web extension himself, it can change the headers and contents of the downloaded resource. Due to a bug in Mozilla Firefox, this is a problem for some sites. That is, in the web extension for Firefox, you will not be able to modify the headers or add a meta tag to cancel the CSP policy for the sites where they are used. This bug has not been closed for several years, although the standards clearly spell out the provisions for web extensions, which states that the policy regarding downloadable resources on the part of the application server cannot be dominant in relation to the web extensions installed by the user.
Restricting a CSP policy can be implemented using the kango framework in the following way:
var browserObject; if(kango.browser.getName() == 'chrome') { browserObject = chrome; } else { browserObject = browser; } var filter = { urls: ["*://*/*"], types: ["main_frame", "sub_frame"] }; var onHeadersReceived = function(details) { var newHeaders = []; for (var i = 0; i < details.responseHeaders.length; i++) { if ('content-security-policy' !== details.responseHeaders[i].name.toLowerCase() && 'x-xss-protection' !== details.responseHeaders[i].name.toLowerCase() ) { newHeaders.push(details.responseHeaders[i]); } } return { responseHeaders: newHeaders }; }; browserObject.webRequest.onHeadersReceived.addListener(onHeadersReceived, filter, ["blocking", "responseHeaders"]);
At the same time, it is necessary not to forget to add lines in the manifest of the web extension, allowing work with the webRequest object in blocking mode:
"permissions": { ... "webRequest": true, "webRequestBlocking": true, ... }
After solving the problem with the restrictions imposed by the CSP, the user can use the scripts written by him on any page on the Internet.
The function call from the internal library is accessible via the global Gc object.
Currently implemented functions:
- GC.saveRow (name, content, [rewrite = 0, async = false]); where name is the name of the rows to write to the collection, content is the data line itself for writing, rewrite is the rewrite flag of the entire collection, used in the Gc.saveRow call (name, 'clear', 1); which deletes all entries in the row collection, async - flag for working in asynchronous mode.
- GC.getRows (name, number, [count = 1, async = false]); where name is the name of the lines in the collection, number is the ordinal number of the line for receiving data, count is the amount of data received starting with number, async is a flag for working in asynchronous mode.
- GC.console (string); where string is the string to be output to the GC console on the page where the script is executed. For example, to demonstrate the progress of a task.
- GC.clearConsole (); The function clears the GC console on the page where the script is running.
- GC.stopScript (); , function to stop the execution of the script.
- GC.loadFile (name, [parseAs = text]); where name is the name of the file with the extension, the contents of which is to be obtained, parseAs is the data preprocessor format, json and text are currently supported.
In the next article I will talk about “
scheduled tasks ”.