需求: 有两个网络环境A,B,需要从B的内网服务中每天定时截取业务信息以便在手机上随时查看。
场景:B没有外网ip。A有外网ip,A没有图片存储服务。
成本最低的方案:A部署一个nginx图片上传的服务。B中使用puppeteer+node访问内网实现截屏,并且调用A的接口上传图片,这样就可以通过A的外网ip查看图片
一,配置A的nginx
A原本部署了一个react项目,针对所有的请求做了拦截操作。只需要在react的location 拦截之前做一下精确匹配即可。
上传图片需要使用nginx upload module 官方文档:http://www.grid.net.ru/nginx/upload.en.html。
[关于一些对location认识的误区]
1、 location 的匹配顺序是“先匹配正则,再匹配普通”。
矫正: location 的匹配顺序其实是“先匹配普通,再匹配正则”。我这么说,大家一定会反驳我,因为按“先匹配普通,再匹配正则”解释不了大家平时习惯的按“先匹配正则,再匹配普通”的实践经验。这里我只能暂时解释下,造成这种误解的原因是:正则匹配会覆盖普通匹配(实际的规则,比这复杂,后面会详细解释)。
2、 location 的执行逻辑跟 location 的编辑顺序无关。
矫正:这句话不全对,“普通 location ”的匹配规则是“最大前缀”,因此“普通 location ”的确与 location 编辑顺序无关;但是“正则 location ”的匹配规则是“顺序匹配,且只要匹配到第一个就停止后面的匹配”;“普通location ”与“正则 location ”之间的匹配顺序是?先匹配普通 location ,再“考虑”匹配正则 location 。注意这里的“考虑”是“可能”的意思,也就是说匹配完“普通 location ”后,有的时候需要继续匹配“正则 location ”,有的时候则不需要继续匹配“正则 location ”。两种情况下,不需要继续匹配正则 location :( 1 )当普通 location 前面指定了“ ^~ ”,特别告诉 Nginx 本条普通 location 一旦匹配上,则不需要继续正则匹配;( 2 )当普通location 恰好严格匹配上,不是最大前缀匹配,则不再继续匹配正则。
总结一句话: “正则 location 匹配让步普通 location 的严格精确匹配结果;但覆盖普通 location 的最大前缀匹配结果”
参见:## 关于一些对location认识的误区
nginx配置
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 开启gzip
gzip on;
# 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
gzip_min_length 1k;
# gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间,后面会有详细说明
gzip_comp_level 4;
# 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
# 禁用IE 6 gzip
gzip_disable "MSIE [1-6]\.";
# 设置压缩所需要的缓冲区大小
gzip_buffers 32 4k;
# 设置gzip压缩针对的HTTP协议版本
gzip_http_version 1.0;
server {
listen 80;
listen 443 ssl;
ssl on;
ssl_certificate ###;
ssl_certificate_key ###;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
server_name www.########;
root /data/program/build/;
index task.html;
location ^~ /download {
autoindex on;
autoindex_exact_size on;
autoindex_localtime on;
alias /tmp/upload_tmp;
}
#只要匹配到文件上传路径就直接不在进行其他匹配
location ^~ /upload/ {
upload_pass /res_upload;
upload_store /tmp/upload_tmp;
upload_store_access user:rw;
upload_limit_rate 0;
upload_set_form_field "${upload_field_name}_name" $upload_file_name;
upload_set_form_field "content_type" $upload_content_type;
upload_set_form_field "tmp_path" $upload_tmp_path;
upload_aggregate_form_field "md5" $upload_file_md5;
upload_aggregate_form_field "size" $upload_file_size;
upload_pass_form_field "^.*$";
}
location / {
try_files $uri @fallback;
}
location @fallback {
rewrite .* /task.html break;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}
二 使用puppeteer+Chrome引擎+node实现网页截屏。
Puppeteer是谷歌官方出品的一个通过DevTools协议控制headless Chrome的Node库。可以通过Puppeteer的提供的api直接控制Chrome模拟大部分用户操作来进行UI Test或者作为爬虫访问页面来收集数据。当然可以用来截屏。
代码
const puppeteer = require('puppeteer');
var https = require('https');
var FormData = require('form-data');
var request = require('request');
var fs = require('fs');
const path="/opt/google/chrome/chrome";
const filePath = "/usr/local/screenShot/filePath/1.png";
//const path="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe";
//const filePath = "C:\\data\\images\\1.png";
(async () => {
const browser = await puppeteer.launch({
headless: true,executablePath: path,args:['--no-sandbox']
});
const page = await browser.newPage();
//注入session 免登录
await page.evaluateOnNewDocument(function() {
sessionStorage.setItem('__token', '###');
sessionStorage.setItem('userInfo','{"token":"###");
});
await page.goto('http://*****/');
await page.setViewport({
width: 1500,
height: 800
});
//截屏操作
setTimeout(async function () {
await autoScroll(page);
try {
await page.screenshot({
fullPage: true,
path: filePath,
//path:"/usr/local/screenShot/1.png"
});
} catch (e) {
console.log("taskLogInfo:screenShotFailed");
console.log(e);
} finally {
console.log("taskLogInfo:systemOut");
//await process.exit();
setTimeout(async function () {
try {
//发送图片到A服务器
const formData = {
custom_file: {
value: fs.createReadStream(filePath),
options: {
filename: '1.png',
contentType: 'multipart/form-data; boundary=----WebKitFormBoundaryGrUwVzlG9ou8k1If'
}
}
};
request.post({url:'https://####/', formData: formData}, function optionalCallback(err, httpResponse, body) {
if (err) {
return console.error('upload failed:', err);
}
console.log('Upload successful! Server responded with:', body);
});
} catch (e) {
console.log("sendfile failed");
console.log(e);
}finally{
//关闭浏览器
await browser.close();
}
}, 5 * 1000);
}
}, 20 * 1000);
})();
//网页滚动加载
function autoScroll(page) {
return page.evaluate(() => {
return new Promise((resolve, reject) => {
var totalHeight = 0;
var distance = 100;
var timer = setInterval(() => {
var scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
})
});
}