Кросс-браузерное веб-расширение для пользовательских скриптов Ч.3

    В этой статье я продолжаю цикл публикаций, в котором я хочу рассказать о своём опыте написания веб-расширения для браузеров. У меня уже был опыт создания веб-расширения, которое установили около 100 000 пользователей Chrome, которое работало автономно, но в данном цикле статей я решил углубиться в процесс разработки веб-расширения тесно интегрировав его с серверной частью.

    imageimageimageimageimage

    Часть 1, Часть 2, Часть 4

    Подводные камни при реализации взаимодействия веб-расширения и серверной части


    Как уже было описано ранее для серверной части используется Meteor.js. Для имплементации RESTful API используется пакет github.com/kahmali/meteor-restivus. Он уже имеет в себе некоторую реализованную часть для покрытия пользовательских механизмов связанных с авторизацией.

    Например, достаточно указать authRequired: true, как в примере ниже, чтобы API point работал только для авторизированных пользователей.

    Api.addRoute('clientScript/:id_script',
      {authRequired: true},
      {get: {
          action: function() {
            //method for GET on htts://example.com/api/v1/clientScript/:id_script
          }
     });
    

    Таким образом были добавлены три API point для регистрации, для получения данных профиля и его обновления, для сброса пароля.

    В самом веб-расширении при вызове методов, требующих авторизацию используется примерно следующий код:

    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
    })
    

    Здесь хорошо виден пример запроса с авторизацией. В заголовках передается X-Auth-Token и X-User-Id, которые были получены в результате процесса регистрации или авторизации. Эти данные хранятся в локальном хранилище веб-расширения и всегда доступны в content.js скрипте.

    Загрузка файлов в веб-расширении сделана через чтение файла на стороне браузера и отправкой через 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;
      });
    

    Здесь важно отметить строку event.target.result.replace(/^data:[^;]*;base64,/, ""). Файл на стороне браузера закодирован в base64, но для совместимости на стороне сервера при использовании этой кодировки в строке Buffer.from(new String(this.bodyParams.content), «base64») мы должны отрезать префикс кодировки и читать только “тело” файла. Также необходимо отметить оборачивание в encodeURIComponent, так как тот же + часто встречается в base64 и именах файлов.

    При редактировании скриптов нужно учитывать кодировку символов в теле скрипта при передаче содержания. В некоторых случаях кодирование base64 не давало правильных результатов при декодировании на стороне сервера при использовании encodeURIComponent. Поэтому предварительно используется принудительное кодирование в utf8 при помощи utf8.encode(str); где mths.be/utf8js v3.0.0 от @mathias

    Скачивание файлов реализовано с использованием хорошо зарекомендованной библиотекой FileSaver. Данные полученные через XHR просто передаются на вход конструктора File, а далее инициализируется скачивание файла:

    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);
    

    Внутренняя библиотека для пользовательских скриптов


    Для взаимодействия скрипта, веб-расширения и серверной части необходимо иметь промежуточное звено, которое позволит быстро получать данные из загруженного файла, сохранять данные после выполнения скрипта и т. д.

    Для этой цели была написана внутренняя библиотека, которая инициализируется перед началом работы любого скрипта путем добавления себя в код страницы. Здесь необходимо добавить информацию о политике защиты источников, для загрузки ресурсов, а именно о content-security-policy.

    Многие сайты используют заголовки с CSP для защиты от выполнения произвольного кода javascript на страницах своих веб-сервисов, таким образом защищаясь от XSS на стороне веб-браузера.

    Так как пользователь самостоятельно устанавливает веб-расширение, оно способно изменять заголовки и содержимое загружаемого ресурса. Из-за бага в Mozilla Firefox, это является проблемой для некоторых сайтов. То есть в веб-расширении для Firefox не получится модифицировать заголовки или добавить meta тэг для отмены политики CSP для сайтов, на которых они используются. Данный баг не закрывают уже несколько лет, хотя в стандартах четко прописано положения для веб-расширений, которое гласит, что политика в отношении загружаемых ресурсов со стороны сервера приложения не может быть главенствующей по отношению к устанавливаемым самим пользователем веб-расширениям.

    Ограничение политики CSP можно реализовать при помощи kango фреймворка следующим способом:

    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"]);
    

    При этом необходимо не забыть в манифесте веб-расширения добавить строки, разрешающие работу с объектом webRequest в блокирующем режиме:

    "permissions": {
    ...
            "webRequest": true,
            "webRequestBlocking": true,
    ...
    }
    

    После решения проблемы с ограничениями накладываемыми со стороны CSP пользователь может применять написанные им скрипты на любой странице в сети интернет.

    Вызов функций из внутренней библиотеке доступен через глобальный объект Gc.
    На текущей момент реализованы функции:

    • GC.saveRow(name, content, [rewrite = 0, async = false]); , где name — имя строк для записи в коллекцию, content — сама строка данных для записи, rewrite — флаг перезаписи всей коллекции, используется в вызове Gc.saveRow(name, ‘clear’, 1); который удаляет все записи в коллекции строк, async — флаг для работы в асинхронном режиме.
    • GC.getRows(name, number, [count = 1, async = false]);, где name — имя строк в коллекции, number — порядковый номер строки для получения данных, count — количество получаемых данных начиная с number, async — флаг для работы в асинхронном режиме.
    • GC.console(string);, где string — строка для вывода в консоль GC на странице, где выполняется скрипт. Например, для демонстрации прогресса выполнения задачи.
    • GC.clearConsole();, функция очищает консоль GC на странице, где выполняется скрипт.
    • GC.stopScript();, функция для остановки выполнения скрипта.
    • GC.loadFile(name, [parseAs = text]);, где name — имя файла с расширением, содержимое которого необходимо получить, parseAs — формат препроцессора данных, на текущий момент поддерживается json и text.

    В следующей статье я расскажу о “задачах по расписанию”.
    Поделиться публикацией

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое