chrome extension webAPP开发小试
1. Chrome extension简介
0x1:为什么需要扩展
简单地说,浏览器插件,可以大大的扩展你的浏览器的功能。包括但不仅限于这些功能:捕捉特定网页的内容,捕捉HTTP报文,捕捉用户浏览动作,改变浏览器地址栏/起始页/书签/Tab等界面元素的行为,与别的站点通信,修改网页内容……,我们可以用它来识别一些网站上的广告代码,并直接把这些代码删掉,这样你就不会受到广告的困扰了。与此同时,也要说浏览器插件的弊端,那就是:会带来一些安全隐患,也可能让你的浏览器变得缓慢甚至不稳定
0x2:基本概念
一个应用(扩展)其实是压缩在一起的一组文件,包括HTML,CSS,Javascript脚本,图片文件,还有其它任何需要的文件。 应用(扩展)本质上来说就是web页面,它们可以使用所有的浏览器提供的API,从XMLHttpRequest到JSON到HTML5全都有。
应用(扩展)可以与Web页面交互,或者通过content script或cross-origin XMLHttpRequests与服务器交互。应用(扩展)还可以访问浏览器提供的内部功能,例如标签或书签等
0x3:文件
每个应用(扩展)都应该包含下面的文件
1. 一个manifest文件 2. 一个或多个html文件(除非这个应用是一个皮肤) 3. 可选的一个或多个javascript文件 4. 可选的任何需要的其他文件,例如图片
在开发应用(扩展)时,需要把这些文件都放到同一个目录下。发布应用(扩展)时,这个目录全部打包到一个应用(扩展)名是.crx的压缩文件中。如果使用Chrome Developer Dashboard,上传应用(扩展),可以自动生成.crx文件
0x4:引用文件
任何需要的文件都可以放到应用(扩展)中,一般的说,可以像在普通的HTML文件中那样使用相对地址来引用一个文件。例如引用images子目录下的文件myimage.png
<img **src="images/myimage.png"**>
0x5:基本架构
绝大多数应用(扩展)都包含一个背景页面(background page),用来执行应用(扩展)的主要功能
上图显示了安装了两个应用(扩展)的浏览器。两个应用(扩展)分别是黄色图标代表的browser action和蓝色图标代表的page action。在background.html文件里定义了browser action和javascript代码。在两个窗口里browser action都可以工作
0x6:页面
背景页面并不是应用(扩展)中唯一的页面。例如,一个browser action可以包含一个弹窗(popup),而弹窗就是用html页面实现的。应用(扩展)还可以使用chrome.tabs.create()或者window.open()来显示内部的HTML文件。
应用(扩展)里面的HTML页面可以互相访问各自DOM树中的全部元素,或者互相调用其中的函数。
下图显示了一个browser action的弹窗的架构。弹窗的内容是由HTML文件(popup.html)定义的web页面。它不必复制背景页面(background.html)里的代码,因为它可以直接调用背景页面中的函数
0x7:保存数据和隐身模式
应用(扩展)可以使用HTML5的 Web Storage API(例如localStorage)来保存数据,或者向服务器发出请求来保存数据。当需要保存数据的时候,首先需要确定是否从隐身模式窗口中发出的请求。缺省情况下,应用(扩展)是不会运行在隐身模式下的,而webapp是会的。需要明确用户在隐身模式下究竟需要应用(扩展)或webapp做什么。
Relevant Link:
https://wizardforcel.gitbooks.io/chrome-doc/content/part2.html
2. 改变浏览器外观
0x1:Browser Actions
用 browser actions 可以在chrome主工具条的地址栏右侧增加一个图标。作为这个图标的延展,一个browser action图标还可以有tooltip、badge和popup
1、Manifest
{ "name": "My extension", ... **"browser_action": { "default_icon": "images/icon19.png", _// optional_ "default_title": "Google Mail", _// optional; shown in tooltip_ "default_popup": "popup.html" _// optional_ }**, ... }
2、UI的组成部分
一个 browser action 可以拥有一个图标,一个tooltip,一个badge和一个popup
1. 图标 Browser action 图标推荐使用宽高都为19像素,更大的图标会被缩小。你可以用两种方式来设置图标: 1) 使用一个静态图片: 使用静态图片适用于简单的应用程序,你也可以创建诸如平滑的动画之类更丰富的动态UI(如canvas element),静态图片可以是任意WebKit支持的格式,包括 BMP,GIF,ICO,JPEG或 PNG 2) 使用HTML5canvas element。 。 2. ToolTip 修改browser_action的manifest中default_title字段,或者调用setTitle()方法。你可以为default_title字段指定本地化的字符串;点击Internationalization查看详情 3. Badge Browser actions可以选择性的显示一个badge— 在图标上显示一些文本。Badges 可以很简单的为browser action更新一些小的扩展状态提示信息。 因为badge空间有限,所以只支持4个以下的字符。 设置badge文字和颜色可以分别使用setBadgeText()andsetBadgeBackgroundColor()。 4. Popup 如果browser action拥有一个popup,popup 会在用户点击图标后出现。popup 可以包含任意你想要的HTML内容,并且会自适应大小。 在你的browser action中添加一个popup,创建弹出的内容的HTML文件。 修改browser_action的manifest中default_popup字段来指定HTML文件, 或者调用setPopup()方法
3、Tips
为了获得最佳的显示效果, 请遵循以下原则
1. 确认 Browser actions 只使用在大多数网站都有功能需求的场景下。 2. 只有在少数网页才有功能的场景, 此场景请使用page actions。 3. 确认你的图标尺寸尽量占满19x19的像素空间。 Browser action 的图标应该看起来比page action的图标更大更重。 4. 不要尝试模仿Google Chrome的扳手图标,在不同的themes下它们的表现可能出现问题,,并且扩展应该醒目些。 5. 尽量使用alpha通道并且柔滑你的图标边缘,因为很多用户使用themes,你的图标应该在在各种背景下都表现不错。 6. 不要不停的闪动你的图标,这很惹人反感
0x2:Context Menus
0x3:桌面通知
0x4:Omnibox
0x5:Override替代页
0x6:Page Actions
使用page actions把图标放置到地址栏里。page actions定义需要处理的页面的事件,但是它们不是适用于所有页面的。下面是一些page actions的示例:
1. 订阅该页面的RSS feed 2. 为页面的图片做一个幻灯片
1、UI的组成部分
同browser actions一样,page actions 可以有图标、提示信息、 弹出窗口。但没有badge,也因此,作为辅助,page actions可以有显示和消失两种状态
使用方法 show() 和 hide() 可以显示和隐藏page action。缺省情况下page action是隐藏的。当要显示时,需要指定图标所在的标签页,图标显示后会一直可见,直到该标签页关闭或开始显示不同的URL(如:用户点击了一个连接)
0x7: 主题
Relevant Link:
https://wizardforcel.gitbooks.io/chrome-doc/content/5.html
3. Optional Permissions
Chrome.permissions用于实现可选权限。在您扩展的运行过程中,而不是在安装的时候请求权限。这能帮助用户清楚为什需要此权限,且仅在必要的时候才运行扩展使用此权限
0x1:实现可选权限
步骤1: 确定哪些权限作为可选,哪些权限作为必选
为了满足基本的功能,扩展需要一些必须的权限,而另一些权限则可以在扩展运行过程再请求用户授予。
可选权限的优势: 1. 扩展激活时仅仅需要少量的权限,扩展运行中需要时才请求用户授予更多的权限。 2. 当扩展运行过程中请求更多权限时,可以更清楚地向用户解释为什么需要这些特定的权限。 3. 可以避免Chrome浏览器禁止扩展升级(原因:当一个扩展的新版本相比老版本需要更多必选权限时,Chrome会阻止这样的扩展的自动升级) 必选权限的优点: 1. 扩展可以一次性提示用户接受所需的权限。 2. 扩展运行过程可以确保拥有相关权限,从而简化了扩展的开发。
步骤2: 在manifest文件中声明可选权限
在extension manifest中用optional_permissions关键字声明可选权限,与声明permissions相同
{ "name": "My extension", ... **"optional_permissions": [ "tabs", "http://www.google.com/" ],** ... }
可以指定下列任何可选权限
host permissions
appNotifications
background
bookmarks
clipboardRead
clipboardWrite
contentSettings
contextMenus
cookies
debugger
history
idle
management
notifications
pageCapture
tabs
topSites
webNavigation
webRequest
webRequestBlocking
步骤3: 请求可选权限
通过调用permissions.request()请求权限,并且需要获得用户授权
document.querySelector('#my-button').addEventListener('click', function(event) { // Permissions must be requested from inside a user gesture, like a button's // click handler. Chrome.permissions.request({ permissions: ['tabs'], origins: ['http://www.google.com/'] }, function(granted) { // The callback argument will be true if the user granted the permissions. if (granted) { doSomething(); } else { doSomethingElse(); } }); });
如果添加与用户看到和接受的权限不同。Chrome会提示用户warning messages。比如,上面的示例代码会导致这样的提示
步骤4: 检查扩展的当前权限
检查扩展是否拥有特定的权限,可以通过permission.contains()实现
Chrome.permissions.contains({ permissions: ['tabs'], origins: ['http://www.google.com/'] }, function(result) { if (result) { // The extension has the permissions. } else { // The extension doesn't have the permissions. } });
步骤5: 删除权限
您应该删除不再需要的权限。当某个用户已授权权限被删除后,使用permissions.request()再次添加此权限时不会再提示用户
Chrome.permissions.remove({ permissions: ['tabs'], origins: ['http://www.google.com/'] }, function(removed) { if (removed) { // The permissions have been removed. } else { // The permissions have not been removed (e.g., you tried to remove // required permissions). } });
Relevant Link:
https://wizardforcel.gitbooks.io/chrome-doc/content/26.html
4. 背景页 - background
扩展常常用一个单独的长时间运行的脚本来管理一些任务或者状态。背景页是一个运行在扩展进程中的HTML页面。它在你的扩展的整个生命周期都存在,同时,在同一时间只有一个实例处于活动状态。
在一个有背景页的典型扩展中,用户界面(比如,浏览器行为或者页面行为和任何选项页)是由沉默视图实现的。当视图需要一些状态,它从背景页获取该状态。当背景页发现了状态改变,它会通知视图进行更新
0x1:清单
一般,背景页不需要任何HTML,仅仅需要js文件,比如
{ "name": "My extension", ... **"background": { "scripts": ["background.js"] }**, ... }
浏览器的扩展系统会自动根据上面scripts字段指定的所有js文件自动生成背景页。
如果您的确需要自己的背景页,可以使用page字段,比如
{ "name": "My extension", ... **"background": { "page": "background.html" }**, ... }
可以用类似于帧之间通讯的方式,直接使用脚本调用在一个扩展的多个页面之间进行通讯。chrome.extension.getViews() 方法会返回属于你的扩展的每个活动页面的窗口对象列表,而chrome.extension.getBackgroundPage() 方法返回背景页
Relevant Link:
https://wizardforcel.gitbooks.io/chrome-doc/content/21.html
5. Content Security Policy (CSP)
为缓解潜在的大规模的跨站点脚本攻击问题,Chrome扩展系统已遵循 Content Security Policy (CSP)的理念,引入了严格的策略使扩展更安全,同时提供创建和实施策略规则的能力,这些规则用以控制扩展(或应用)能够加载的资源和执行的脚本。
通常情况下,CSP通过黑白名单的机制控制资源加载(或执行脚本)。为您的扩展制定一个合理的策略以使您认真地考虑哪些资源是扩展需要的,并让浏览器确保仅仅这些资源是您的扩展需要访问的。 这些策略为您的扩展提供host permissions之外的额外安全保障;这是一个额外的一层保护,而不是一种取代。
在Web上,此策略是通过HTTP头或meta元素定义的。在Chrome扩展系统中,不存在这两种方式。扩展是通过manifest.json文件定义的
{ ..., "content_security_policy": "[POLICY STRING GOES HERE]" ... }
0x1:默认策略限制
没有定义 manifest_version 的扩展安装包默认是没有内容安全策略的。定义manifest_version为2的扩展才会默认开启内容安全策略
script-src 'self'; object-src 'self'
0x2:不执行Inline JavaScript
Inline JavaScript和eval一样危险,将不会被执行。这条规则将同时禁止内嵌<script>块和内联事件(例如: <button onclick="...">)
这条规则通过禁止第三方脚本避免大量的跨脚本攻击。同时它还强制您编写内容与表现分离的代码
0x3:只加载本地脚本和资源
只有扩展包内的脚本和资源才会被加载!通过Web即时下载的将不会被加载! 这确保您的扩展只执行已经打包在扩展之中的可信代码,从而避免了线上的网络攻击者通过恶意重定向您所请求的Web资源所带来的安全隐患。
在编写基于jQuery(或者其它库)的代码时,在扩展包中直接包含此库文件而不是从外部内容分发网络(CDN)在线加载。 换言之,不再这样使用
<!doctype html> <html> <head> <title>My Awesome Popup!</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> </script> </head> <body> <button>Click for awesomeness!</button> </body> </html>
而是首先下载此js文件,并包含到扩展包里,然后修改代码为
<!doctype html> <html> <head> <title>My Awesome Popup!</title> <script src="jquery.min.js"></script> </script> </head> <body> <button>Click for awesomeness!</button> </body> </html>
0x4:放宽默认策略
没有绕过“禁止Inline JavaScript执行”的办法。即使有意在脚本策略字段增加unsafe-inline也是没有任何作用的。就是这样设计的!
不过,如果您需要一些外部的JavaScript或者资源,您可以通过将HTTPS源的脚本加入白名单来放宽“只加载本地脚本和资源”策略。 请记住:白名单对于不安全的HTTP源是无效的。这也是特意设计的!因为必须确保可执行资源的加载正是您所期望的,而不是被网络攻击者替换过的
{ .. "content_security_policy": "script-src 'self' https://ajax.googleapis.com; object-src 'self'" }
Relevant Link:
http://open.chrome.360.cn/extension_dev/contentSecurityPolicy.html
6. getstarted
http://files.cnblogs.com/guogangj/chrome-plugin-page-action-demo.7z
0x1:mainifest.json
{ "manifest_version": 2, "name": "cnblogs.com viewer", "version": "0.0.1", "background": { "scripts": ["background.js"] }, "permissions": ["tabs"], "page_action": { "default_icon": { "19": "cnblogs_19.png", "38": "cnblogs_38.png" }, "default_title": "cnblogs.com article information" } }
注意,"permissions"属性里的"tabs"是必须的,否则下面的js不能获取到tab里的url,而这个url是我们判断是否要把小图标show出来的依据
0x2:background.js
一旦插件被启用(有些插件对所有页面都启用,有些则只对某些页面启用),chrome就给插件开辟了一个独立的javascript运行环境(又称作运行上下文),用来跑你指定的background script,在这个例子中,也就是background.js
function getDomainFromUrl(url){ var host = "null"; if(typeof url == "undefined" || null == url) url = window.location.href; var regex = /.*\:\/\/([^\/]*).*/; var match = url.match(regex); if(typeof match != "undefined" && null != match) host = match[1]; return host; } function checkForValidUrl(tabId, changeInfo, tab) { if(getDomainFromUrl(tab.url).toLowerCase()=="www.cnblogs.com"){ chrome.pageAction.show(tabId); } }; chrome.tabs.onUpdated.addListener(checkForValidUrl);
代码中,我们使用了一个正则表达式去匹配url,获取出其中的domain部分,如果domain部分是“www.cnblogs.com”的话,就把小图标show出来,效果如下
Relevant Link:
http://www.cnblogs.com/guogangj/p/3235703.html
7. popup example
0x1:manifest.json
{ "name": "My Bookmarks", "version": "1.1", "description": "A browser action with a popup dump of all bookmarks, including search, add, edit and delete.", "permissions": [ "bookmarks" ], "browser_action": { "default_title": "My Bookmarks", "default_icon": "icon.png", "default_popup": "popup.html" }, "manifest_version": 2, "content_security_policy": "script-src 'self' https://ajax.googleapis.com; object-src 'self'" }
0x2:popup.js
// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Search the bookmarks when entering the search keyword. $(function() { $('#search').change(function() { $('#bookmarks').empty(); dumpBookmarks($('#search').val()); }); }); // Traverse the bookmark tree, and print the folder and nodes. function dumpBookmarks(query) { var bookmarkTreeNodes = chrome.bookmarks.getTree( function(bookmarkTreeNodes) { $('#bookmarks').append(dumpTreeNodes(bookmarkTreeNodes, query)); }); } function dumpTreeNodes(bookmarkNodes, query) { var list = $('<ul>'); var i; for (i = 0; i < bookmarkNodes.length; i++) { list.append(dumpNode(bookmarkNodes[i], query)); } return list; } function dumpNode(bookmarkNode, query) { if (bookmarkNode.title) { if (query && !bookmarkNode.children) { if (String(bookmarkNode.title).indexOf(query) == -1) { return $('<span></span>'); } } var anchor = $('<a>'); anchor.attr('href', bookmarkNode.url); anchor.text(bookmarkNode.title); /* * When clicking on a bookmark in the extension, a new tab is fired with * the bookmark url. */ anchor.click(function() { chrome.tabs.create({url: bookmarkNode.url}); }); var span = $('<span>'); var options = bookmarkNode.children ? $('<span>[<a href="#" id="addlink">Add</a>]</span>') : $('<span>[<a id="editlink" href="#">Edit</a> <a id="deletelink" ' + 'href="#">Delete</a>]</span>'); var edit = bookmarkNode.children ? $('<table><tr><td>Name</td><td>' + '<input id="title"></td></tr><tr><td>URL</td><td><input id="url">' + '</td></tr></table>') : $('<input>'); // Show add and edit links when hover over. span.hover(function() { span.append(options); $('#deletelink').click(function() { $('#deletedialog').empty().dialog({ autoOpen: false, title: 'Confirm Deletion', resizable: false, height: 140, modal: true, overlay: { backgroundColor: '#000', opacity: 0.5 }, buttons: { 'Yes, Delete It!': function() { chrome.bookmarks.remove(String(bookmarkNode.id)); span.parent().remove(); $(this).dialog('destroy'); }, Cancel: function() { $(this).dialog('destroy'); } } }).dialog('open'); }); $('#addlink').click(function() { $('#adddialog').empty().append(edit).dialog({autoOpen: false, closeOnEscape: true, title: 'Add New Bookmark', modal: true, buttons: { 'Add' : function() { chrome.bookmarks.create({parentId: bookmarkNode.id, title: $('#title').val(), url: $('#url').val()}); $('#bookmarks').empty(); $(this).dialog('destroy'); window.dumpBookmarks(); }, 'Cancel': function() { $(this).dialog('destroy'); } }}).dialog('open'); }); $('#editlink').click(function() { edit.val(anchor.text()); $('#editdialog').empty().append(edit).dialog({autoOpen: false, closeOnEscape: true, title: 'Edit Title', modal: true, show: 'slide', buttons: { 'Save': function() { chrome.bookmarks.update(String(bookmarkNode.id), { title: edit.val() }); anchor.text(edit.val()); options.show(); $(this).dialog('destroy'); }, 'Cancel': function() { $(this).dialog('destroy'); } }}).dialog('open'); }); options.fadeIn(); }, // unhover function() { options.remove(); }).append(anchor); } var li = $(bookmarkNode.title ? '<li>' : '<div>').append(span); if (bookmarkNode.children && bookmarkNode.children.length > 0) { li.append(dumpTreeNodes(bookmarkNode.children, query)); } return li; } document.addEventListener('DOMContentLoaded', function () { dumpBookmarks(); });
0x3:popup.html
在弹出的html页面代码中引入js文件,避免inline js的不安全做法
0x4:插件的结构
1. manifest.json作为插件的配置文件,同时可以看作程序的“入口”,因为它指定了显示什么图标,background script有哪些文件,content script又有哪些文件,pop up的页面是什么,等等。 2. content script就是我们要注入到页面中的脚本,插件允许我们往网页中注入脚本,这是一个让人有想象力的功能,总的来说,就是让我们全面干预页面的内容!也许你马上会想到,这可能带来很大的安全隐患,没错,有些恶意插件会窃取你的页面信息,而有些有漏洞的插件则可能让你遭受跨站脚本注入(XSS)的攻击;另一个可能你会想到的问题是:往页面中注入自己的脚本,难道不会跟页面原本的脚本发生冲突吗?能想到这点说明你真的很厉害,如果我们的注入脚本和页面原本的脚本处于同一个运行环境中,确实会发生冲突,所以,Chrome是另外开辟了一个独立的运行空间,供我们的Content Script使用的,Content Script能访问DOM的内容,但却不能访问页面原本的脚本(我是说直接访问不行),反之,页面原本的脚本也不能直接访问Content Script 3. 在图中,浅红色的背景块代表Content Script的运行环境,而浅蓝色的背景块代表页面运行环境,另外插件的运行环境我用浅绿色表示,注意,这是三个不同的运行环境,调试的时候你会充分体会到它们的不同。 4. Content Script会在什么时候运行呢?默认情况下,是在网页加载完了和页面脚本执行完了,页面转入空闲的情况下(Document Idle),但这个是可以改变的,详情可参考https://developer.chrome.com/extensions/content_scripts.html,查看其中的“run_at”。 5. 由于处于不同的运行环境中,Content Script和Background Script不能直接互相访问,那它们之间如何通信?通过Message!
Relevant Link:
http://www.cnblogs.com/guogangj/p/3235703.html https://developer.chrome.com/extensions/getstarted https://developer.chrome.com/extensions/samples#search: