时隔一年回到家,弟弟还是长期沉迷于各种动画片,考虑到之前更改host会将整个视频网站禁掉,无法查看某些学习视频(最后还得手动给他解禁hhhh),因此决定编写一个浏览器插件实现对内容的过滤。
环境
本地开发环境:
- Windows 10
- Chrome浏览器
服务器环境:
- Ubuntu 20.04
- Python
需求分析
因为在上一次设置了安装新应用需要输入密码,因此将电脑其他浏览器卸载掉,只安装Chrome浏览器,就可以使其无法绕过我们编写的插件了(至于弟弟是否会卸载插件,可以对插件进行一定的伪装使他意识不到这个问题)。
然后,就是插件的功能需求,主要有以下几点:
- 禁掉各种主流的动画片
- 禁掉各种网页小游戏
- 禁掉各种涩情暴力等不利于儿童身心的网页内容
- 保留各类科普、学习相关的网页
程序设计
根据以上的需求分析,可以编写一个浏览器插件,对网页标题进行校验,如包含熊出没
、喜羊羊
等关键词,则跳转页面至其他页面。
考虑到仅通过前端校验的方式,当关键词更新时就需要重新打包安装插件,无法实现程序的快速迭代和更新,因此,考虑增设后台程序,前端将获取到的网页标题发送给后台,由后台进行校验返回布尔值,前端接收到布尔值后再进行禁掉或放行。
程序编写
浏览器插件框架
首先,搭建一个浏览器插件的框架。
新建一个文件夹,这里以插件名称命名为DDblocker
,然后找一个图标,新建一个名为manifest.json
的文件:
{
"manifest_version": 2,
"name": "DDBlocker",
"version": "1.0",
"description": "Filter the video list before play it.",
"icons": {
"48": "icons/border-48.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["ddblocker.js"]
}
]
}
其中,name
是插件名称,可以随意取名,description
为程序描述,icons
则填写之前的图标路径(48
表示图标的长宽为48*48像素,这里最好同时提供48
和96
像素长宽的图标),content_scripts
中的matches
表示在何处启用插件,这里的all_urls
表示所有网址都启用,而js
则为插件js函数的入口,也就是插件的主要内容,后续程序编写都是在这个js文件内。
新建文件,文件名为上文配置文件中定义的名称,这里是ddblocker.js
,打开文件,就可以开始编写功能函数了。
最终文件目录:
插件功能函数
插件主要分为两个功能,分别是获取页面标题请求服务器和根据服务器的返回值执行对应操作。
首先,书写根据服务器返回值执行对应操作的函数:
function dd_callback(response_value){
if (response_value == true){
// contine as default.
return true;
}else{
console.log(response_value);
location.href = 'about:blank';
}
}
当返回值为true
时不执行任何操作,即放行;当返回值为false
时跳转页面至空白页。
然后将该段函数插入到页面中去:
function adding_elements(){
// dd function
var a = document.createElement("script");
a.type = 'text/javascript';
a.innerHTML = dd_callback.toString();
document.body.appendChild(a);
}
adding_elements();
这里将上述函数通过toString()
转换为字符串,构建一个script
标签,插入到页面之中。
然后,需要书写请求服务器的函数。考虑到浏览器存在跨域问题,因此这里采用jsonp的方式进行跨域:
function jsonp_request(){
// request script
var b = document.createElement('script');
title = document.title;
b.src = build_url('dd_callback', title);
document.body.appendChild(b);
}
通过document.title
获取标签页标题,通过插入script
标签,将其src指定为远端服务器的地址(这里使用函数build_url()
来进行URL构建),来发送跨域请求,构建url的函数如下:
function build_url(callback_f, title){
url = 'http://ddblocker.xxx.com:5000/ddblocker?title=' + title;
// adding callback function.
url += '&callback=' + callback_f;
return url;
}
这里远程服务器开启了5000
端口,接口为ddblocker
,接收title
和callback
两个参数,其中,title
用于服务器判断是否应该放行该网址,而callback
用于返回前端,以执行回调函数。
服务器端程序编写
由于所有的判断操作在服务器端执行,因此,需要书写服务器端代码,这里使用flask作为后端代码的web服务。
首先,新建一个flask应用,并定义接口:
from flask import Flask, request
app = Flask('ddblocker')
@app.route('/ddblocker')
def hello_dd():
pass
if __name__ == "__main__":
app.run(
host='0.0.0.0',
port=5000,
# debug=True
)
然后编写函数内容:
def hello_dd():
# server logic here.
# request -> data, document.title from client area.
# server should allow jsonp protocol and return javascript which call the target function with args 'true' or 'false'.
title = request.args.get('title')
print(title)
callback_f = request.args.get('callback')
if not title or not callback_f:
return 'location.href="about:blank"'
return build_function_to_javascript( callback_f, title )
首先,接收传过来的参数,然后判定其是否为空,任一参数为空即返回跳转页面至空白页的js代码,否则返回执行dd_callback
函数的js代码(该代码通过函数build_function_to_javascript
进行构造)。
构建返回的js代码的函数如下:
def build_function_to_javascript(function_name, function_args):
args_to_string = '(' + check_function(function_args) + ')'
js_code = function_name + args_to_string
return js_code
其中,通过字符串拼接的方式构建出形如dd_callback(true)
的js代码,代码中的参数使用函数check_function
来进行判断是否放行。
判断标签页标题是否符合要求的代码如下:
import pickle
# init
# read pickle file save the lists.
try:
with open('./banned_list.pkl','rb') as f:
banned_list = pickle.load(f)
except:
banned_list = set() # empty
def check_function(title):
for t in banned_list:
if t in title:
return 'false'
return 'true'
这里使用了文件存储禁用词条,通过黑名单的方式进行判断。读取文件,遍历文件中的每个词条,判定标题是否包含该关键词,是的话则返回false
,否则返回true
。
为了便于添加词条,设定一个添加词条的接口:
@app.route('/Blocker_changer')
def manager():
operation_t = request.args.get('type')
title = request.args.get('title')
if not operation_t or not title:
# format not supported.
# return empty.
return 'Error operations'
else:
# check operations and do something here.
if operation_t == 'add':
banned_list.add(title)
return 'adding success'
elif operation_t == 'remove':
try:
banned_list.remove(title)
except:
# pass
return 'title does not exists'
return 'remove success'
else:
return 'error oprations'
该接口接收title
和type
作为参数,分别表示禁用词和对应操作(添加词条或删除词条),将其存入之前读取的文件内容的全局变量。
当程序中断时,需要将当前文件内容保存下来,因此需要捕获一些程序中断运行的各类异常信号,因此,新增程序中断的操作函数:
def signal_handler( sig, frame):
print(sig)
with open('./banned_list.pkl','wb') as f:
pickle.dump(banned_list, f)
raise KeyboardInterrupt
然后将信号捕捉语句添加到主函数内:
if __name__ == "__main__":
signal.signal( signal.SIGINT, signal_handler )
signal.signal( signal.SIGTERM, signal_handler )
signal.signal( signal.SIGSEGV, signal_handler )
app.run(
host='0.0.0.0',
port=5000
)
插件运行
打开Chrome浏览器,点击右上角的按钮,依次选择更多工具
->扩展程序
:
然后单击右上角的按钮打开开发者模式,点击左边的按钮加载已解压的扩展程序
,选择我们建立的项目文件夹:
然后就可以看到插件加载成功,点击右下角的按钮启用插件:
然后随意打开一个页面,这里以百度为例,在页面右键单击,选择检查
,查看控制台,发现插件报错:
报错提示
ddblocker.js:32 Mixed Content: The page at 'https://www.baidu.com/' was loaded over HTTPS, but requested an insecure script 'http://ddblocker.xxxxx.com:5000/ddblocker?title=%E7%99%BE%E5%BA%A6%E4%B8%80%E4%B8%8B%EF%BC%8C%E4%BD%A0%E5%B0%B1%E7%9F%A5%E9%81%93&callback=dd_callback'. This request has been blocked; the content must be served over HTTPS.
,大概意思就是因为百度页面为HTTPS,而插件请求的地址为http,请求被锁定,必须使用HTTPS才行。因此,这里需要给服务器配置HTTPS,需要一个SSL证书。
申请证书需要一个域名,我使用的是阿里云的域名服务,因此打开阿里云的控制台,搜索SSL证书
,搜索完成后点击左侧SSL证书
,然后点击免费证书
,点击立即购买
:
在购买界面依次勾选以下选项(这里由于已经购买了因此无法重复申请):
点击购买后,由于金额为0,因此会直接跳转到成功界面。然后在之前的界面选择证书申请,填写自己的域名(这里填写一个子域名)和信息即可,然后点击下一步:
然后按照图示内容新增一条解析记录,完事后点击验证
按钮,通过后点击提交审核
:
然后就会看到之前的列表处多出了一个证书,点击下载按钮:
选择其他
下载:
下下来解压之后会看到两个后缀分别为pem
和key
的文件:
将其和服务器的脚本放在同一文件夹内,更改脚本主函数内容为:
if __name__ == "__main__":
app.run(
host='0.0.0.0',
port=5000,
ssl_context=('./7219406_ddblocker.xxx.com.pem','7219406_ddblocker.xxx.com.key')
)
将前端的插件脚本请求的网址的请求头改成https
。
使用命令运行脚本:
python ddblocker.py
打开新的标签页,访问链接https://ddblocker.xxx.com:5000/Blocker_changer?type=add&title=熊出没
,显示adding success
:
然后重新导入前端的插件。打开新的标签页,访问百度,发现显示正常,然后打开B站,搜索熊出没,随意点开一个视频,发现页面跳转至了空白页,插件实现成功:
后续迭代与展望
- 可对插件进行一定的伪装,例如将其不显示在浏览器书签栏右侧,将其名称和图标更改为一些不易被察觉的东西
- 可在前端再添加一个字段,将其浏览的网址也传输到服务器,服务器对每次收到的请求的标题和网址都进行存储,实现对弟弟浏览内容的监控
- 可做一个前端可视化界面实现词条的添加与删除,更加方便
- 对服务器进行鉴权操作,防止弟弟发觉之后反向操作删除所有字段hhhh