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 web extensions and server side


    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 enough 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, the following code is used when calling methods that require authorization:

    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, which were obtained as a result of the registration or authorization process, are transmitted. 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]);
    	}
    	returnfalse;
      });
    

    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 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, forceful encoding in utf8 is previously used 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, web extension and server part, you need 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 that is initialized before any script starts working. add yourself to the page code. Here it is necessary to add information about the policy of protecting sources, for loading resources, namely about 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, it will not be possible to modify the headers or add a meta tag to cancel the CSP policy for the sites on which they are used. This bug has not been closed for several years, although the standards clearly spell out 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 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 web extension manifest that allow working 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 to receive 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 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 must be obtained, parseAs is the format of the data preprocessor, json and text are currently supported.

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

    Also popular now: