Переводим Chrome extension на manifest_version 2

в 9:27, , рубрики: chrome, chrome extension, content security policy, csp, extension, Google Chrome, sandboxing, templates, underscore, Web-store, метки: , , , , , , , ,

Владельцам расширений (а также приложений) для Хрома уже пора бы задуматься над поддержкой второй версии манифеста.
Если кто не в курсе, то не так давно были объявлены новые изменения и нововведения в разработку расширений для браузера.
Далее будет выборочный перевод двух страниц и мой способ использования шаблонизатора изнутри песочницы.

Сначала немного о планах Гугла по поддержке старых расширений

* Далее старыми расширениями буду называть расширения и приложения с версией манифеста 1 (или вообще без версии).

  • Начиная с Chrome 21 блокируется создание новых расширений с первой версией манифеста, но разрешаются обновления существующих расширений до старой версии манифеста.
  • С выходом Chrome 23 (начало ноября) у Веб-сторе будет заблокировано обновление на расширения со старыми версиями манифеста. Хром перестанет запаковывать старые расширения и загружать распакованные для разработки.
  • Первая четверть 2013 — старые расширения уже будет не найти в Веб-сторе. Разработчикам об этом сообщат по email.
  • Вторая четверть 2013 — из Веб-стора будут удалены все старые расширения, а разработчикам придет еще одно уведомление. Но Хром пока будет загружать и запускать установленные расширения с манифестом версии 1.
  • Третья четверть 2013 — Хром перестанет загружать и запускать старые расширения.

Различия версий манифеста 1 и 2

  • Политика безопасности контента (content security policy) по умолчанию установлена в `script-src 'self'; object-src 'self'. Это по сути самое важное обновление. О нем немного позже.
  • Все ресурсы расширения теперь недоступны по URL chrome-extension://[PACKAGE ID]/[PATH]. Т.е. вы не сможете подключить скрипт или картинку с расширения с других страниц кроме самого расширения. Но чтобы обойти этот недостаток появилось свойство web_accessible_resources, в котором можно указать массив с путями к нужным ресурсам.
  • Вместо свойства background_page (которое было строкой), теперь пишем background, которое должно содержать объект со свойством scripts или page.
  • Изменения в browser actions:
    • поле browser_actions заменено на browser_action, а API chrome.browserActions — на chrome.browserAction.
    • удалено свойство icons из browser_action. Вместо него нужно использовать default_icon или chrome.browserAction.setIcon.
    • удалено свойство name из browser_action. Вместо него нужно использовать default_title или chrome.browserAction.setTitle.
    • удалено свойство popup из browser_action. Вместо него нужно использовать default_popup или chrome.browserAction.setPopup.
    • свойство default_popup в browser_action должно быть строкой, а не объектом

  • Изменения в page actions:
    • поле page_actions заменено на page_action, а API chrome.pageActions — на chrome.pageAction.
    • удалено свойство icons из page_action. Вместо него нужно использовать default_icon или chrome.pageAction.setIcon.
    • удалено свойство name из page_action. Вместо него нужно использовать default_title или chrome.pageAction.setTitle.
    • удалено свойство popup из page_action. Вместо него нужно использовать default_popup или chrome.pageAction.setPopup.
    • свойство default_popup в page_action должно быть строкой, а не объектом.
    • Удалено chrome.self из API, теперь нужно использовать chrome.extension.

  • Больше нет chrome.extension.getTabContentses и chrome.extension.getExtensionTabs. Вместо них нужно использовать chrome.extension.getViews({ "type": "tab" }).
  • Вместо Port.tab используем Port.sender.

Политика безопасности контента (Content Security Policy или CSP)

Чтобы расширения были менее подвержены XSS уязвимостям были внедрены общие принципы CSP. В общем CSP являет собой механизм белых и черных списков касательно ресурсов, которые загружаются и выполняются расширением. С помощью CSP можно установить только необходимые разрешения для расширения и таким образом повысить его безопасность.
Эта политика является дополнительным уровнем защиты над правами на доступ к ресурсам (host permissions)

Установить политику безопасности можно в строковом параметре content_security_policy в manifest.json.
Если не установлена версия манифеста (manifest_version), то по умолчанию нет никакой политики безопасности контента. Но для второй версии манифеста она установлена по умолчанию со значением script-src 'self'; object-src 'self'. Поэтому есть некоторые ограничения. Например, функция eval выполнятся не будет. Так же не будут выполнятся инлайновые <script> блоки и инлайновые обработчики событий (<button onclick="...">). Если вы в коде по какой-то причине передавали строку в качестве первого аргумента функций setTimeout и setInterval, то это тоже придется исправить.
Так же скрипты, подключаемые с других ресурсов (с CDN, например), нужно сохранить локально.

Смягчение ограничений политики безопасности

К сожалению нет способа смягчить ограничения на выполнение инлайновых скриптов. Но есть возможность подключать сторонние скрипты (правда, только по https). Для этого нужно указать домен в content_security_policy, например так:

{
  ...,
  "content_security_policy": "script-src 'self' https://example.com; object-src 'self'",
  ...
}

“Что же делать если нужно выполнять eval?” или “Как подключить шаблонизатор?”

Сначала вы можете подумать — зачем мне eval? Но эта функция нужна почти для всех шаблонизаторов (заметьте, new Function() тоже не работает). Здесь нам поможет песочница (sandbox).
Можно создать отдельную страницу, где будут выполнятся все небезопасные операции (например, eval) и запускать ее изнутри песочницы (на песочницу по умолчанию CSP не распространяется).
Так же есть возможность передавать данные между расширением и песочницей через метод postMessage().

Далее расскажу как я подключал underscore шаблонизатор

  1. Создадим файл sandboxed/template-renderer.html с таким вот контентом
    template-renderer.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Sandboxed Template Renderer</title>
        <script src="/js/libs/underscore/underscore-min.js"></script>
    </head>
    <body>
        <script>
            var templates = {};
            window.addEventListener('message', function (event) {
                var template;
                if (typeof templates[event.data.templateName] == 'undefined') {
                    template = _.template(event.data.template);
                    templates[event.data.templateName] = template;
                } else {
                    template = templates[event.data.templateName];
                }
                event.source.postMessage({
                    id: event.data.id,
                    result: template(event.data.context)
                }, event.origin);
            });
        </script>
    </html>
    

  2. В манифесте добавим этот html в песочницу
    {
    ...,
        "sandbox": {
            "pages": ["sandboxed/template-renderer.html"]
        },
    ...
    }
    

  3. Создадим функцию которая будет обращаться к нашей песочнице за рендерингом шаблона
    function getTemplate
    var getTemplate = (function(){
    
        var iframe = document.createElement('iframe'),
            callbacks = [];
    
        iframe.src = 'sandboxed/template-renderer.html';
        iframe.style.display = 'none';
        document.body.appendChild(iframe);
    
        window.addEventListener('message', function (event) {
            callbacks.forEach(function (item, idx) {
                if (item && item.id == event.data.id) {
                    item.callback(event.data.result);
                    delete callbacks[idx];
                }
            });
        });
    
        return function (templateName, template) {
            return function (context, callback) {
                var id = Math.random();
                callbacks.push({
                    id: id,
                    callback: callback
                });
                iframe.contentWindow.postMessage({
                    id: id,
                    templateName: templateName,
                    template: template,
                    context: context
                }, '*');
            };
        };
    }());
    

  4. Шаблонизатор готов к использованию
    // получаем функцию, которая будет рендерить шаблон в зависимости от контекста
    var template = getTemplate('templateId', templateContent);
    // одно плохо - теперь результат получаем асинхронно
    template({text: 'Hello world'}, function (html) {
        // выводим html на страницу
        // можно было передать $('body').html в качестве второго параметра,
        // но решил написать так для большей наглядности
        $('body').html(html);
    });
    

Это решение — первое что пришло в голову. Наверное есть способ сделать это лучше (красивее). Буду рад увидеть предложения в комментариях.

И на последок, мой manifest.json:

manifest.json

{
    "name": "Twittext",
    "description": "A lightweight Google Chrome extension for Twitter",
    "background": {
        "page": "background.html"
    },
    "manifest_version": 2,
    "browser_action": {
        "default_icon": "img/icon_19.png",
        "default_title": "Twittext",
        "default_popup": "popup.html"
    },
    "icons": {
        "128": "img/icon_128.png",
        "19": "img/icon_19.png",
        "48": "img/icon_48.png"
    },
    "options_page": "options.html",
    "version": "1.6.1",
    "permissions": [
        "tabs",
        "background",
        "https://api.twitter.com/",
        "https://userstream.twitter.com/"
    ],
    "sandbox": {
        "pages": ["sandboxed/template-renderer.html"]
    }
}

Автор: utf

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js