Cross-browser web extension for custom Part 3 scripts

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.

imageimageimageimageimage

Part 1 , Part 2 , Part 4

Pitfalls 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() { //method for GET on htts://example.com/api/v1/clientScript/:id_script } }); 

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) { //code for response handler }) 

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") { //ok } else { //error } } else { if(data.status == 401) { //notAuth } else { //error } } }); }; if (event.currentTarget.fileInput.files.length != 0) { reader.readAsDataURL(event.currentTarget.fileInput.files[0]); } return false; }); 

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:


In the next article I will talk about “ scheduled tasks ”.

Source: https://habr.com/ru/post/416585/


All Articles