前言
最近项目有个检测上传文件病毒扫描的功能,需求是页面上通过上传控件,上传文件到服务器,然后后端扫描文件流,并且返回扫描结果。 逻辑不复杂,但有个致命问题 ,调试时得弄个病毒文件测试,但是由于公司电脑里都安装了杀毒软件,病毒文件刚创建出来了瞬间就被杀毒给删了,没文件咋上传?
真的就没办法在公司电脑里调试真实病毒文件了吗?
思路
先不考虑杀毒软件,首先看下怎么创建出来病毒文件,很简单,用下面这段字符创建一个txt纯文本文件,就是一个病毒文件。
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
这段文本是EICAR测试文件的字符串
,一般用来检测杀毒软件的,可以用任意类型文件,比如文件起名为test.png``test.pdf
都可以当做病毒文件,而且这种文件本身并不是病毒,不会对电脑系统造成任何影响。
然后分析下怎么不用创建实体文件来上传,文件上传本质都是用的<input type="file"
元素的change
事件来实现的,change
时获取dom
上的files
属性来获取上传的文件信息,那这个files
属性是否可以自己创建然后替换呢?
下面代码就是创建一个内容为纯文本的File
对象,只需要把dom
上的files
给替换掉就行了。
javascript
const virusText = `X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`;
const file = new File([virusText], 'test.txt', { type: 'text/plain'});
Demo
先来写个Demo调试下。
用Nest创建一个文件上传api npx nest new nest-app
typescript
@Post('upload')
@UseInterceptors(
FileInterceptor('file'),
)
uploadFile(
@UploadedFile() file: Express.Multer.File
) {
console.log('nest file', file);
return true;
}
html
<input type="file" id="input1" />
javascript
const input1 = document.getElementById('input1');
input1.onchange = function (e) {
// const input = e.target || e.dataTransfer;
const file = input1.files[0];
console.log('input change', file.name, input1.value);
const url = '/api/upload';
const formData = new FormData();
formData.append('file', file);
window.fetch(url, {
method: "POST",
mode: 'cors',
body: formData,
});
// 每次上传文件后清空input value,可以重复多次上传同一文件
input1.value = '';
}
一个很简单的上传文件例子,测试下效果,新旧一个txt,上传。
nest api里打出了log,文件名和size都正确。
方案1:替换File对象
然后尝试用上面的新建File
对象,在上例input change
事件里替换File
对象。
javascript
// input change事件
const virusText = `X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`;
const file = input.files[0];
// 用原文件名和type替换
const newFile = new File([virusText], file.name, { type: file.type });
// 或者直接定义一个txt
const newFile = new File([virusText], 'test.txt', { type: 'text/plain'});
// 用newFile调用api上传
const formData = new FormData();
formData.append('file', newFile);
...
测试下效果。
68个字符,正好跟病毒测试字符串长度一致(多一个转义字符),你也可以在nest api里把buffer返回到前台download下来,会发现被杀毒软件删除了,也就说明传给后端的文件流确实是病毒文件。
api返回buffer浏览器download代码参考:
javascript
// nest
const { originalname } = file;
const fileExtension = originalname.substring(originalname.lastIndexOf('.') + 1);
const output = 'dest.' + fileExtension;
res.set('Content-Disposition', `attachment; filename="${output}"`);
res.send(file.buffer);
// js
window.fetch(...)
.then(res => res.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
方案2:假如不能改代码
上例是通过修改了change事件里的上传代码来实现的,那假如不能改代码呢?比如下面场景:
- 比如测试一个线上环境;
- 比如上传逻辑封装到了第三方里,无法修改。
这时就能想到另一种方案,上面说了,上传文件本质就是监听input元素的change事件,那能不能有方法主动触发change事件呢?同时在input dom的files属性里动态添加文件。
javascript
window._test = function (fileName, index = 0) {
// 根据参数序号获取input dom
const input = document.querySelectorAll('input[type="file"]')[index];
if (!input) return;
const virusText = `X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`;
const typeMap = {
'txt': 'text/plain',
'png': 'image/png',
'zip': 'application/zip',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pdf': 'application/pdf',
};
// 获取文件扩展名
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
// 获取MIME type类型
const type = typeMap[fileExtension] || 'text/plain';
// 创建File对象
const file = new File([virusText], fileName, { type });
// 创建FileList对象
const fileList = new ClipboardEvent("").clipboardData || new DataTransfer();
fileList.items.add(file);
// 用Object.defineProperty更改input dom files值
Object.defineProperty(input, "files", {
value: fileList.files,
configurable: true,
});
// 用Object.defineProperty更改input dom value值
Object.defineProperty(input, "value", {
value: `C:\\fakepath\\${file.name}`,
configurable: true,
});
// 触发input change事件
const event = new Event("change", { bubbles: true });
input.dispatchEvent(event);
// 最后清空dom的files和value值,这样就可以继续手动上传文件
delete input['files'];
delete input['value'];
}
知识点:
document.querySelectorAll
获取所有input type=file,然后按参数序号找dom,这里也可以直接传dom,都行。MIME type类型
的获取,这里只定义了部分与扩展名的映射,其它的可以自行在网上找。FileList
对象创建,这里也可以直接用js Array,主要看项目change
方法里是怎么获取的文件。Object.defineProperty
由于安全限制问题,input type=file的dom的files\value属性无法直接更改,需要用Object.defineProperty
方式更改。delete input['files\value']
最后需要清空dom的files和value值,这样就可以继续手动上传文件。
这样就可以直接在浏览器F12 console
里运行上面的代码,不需要改代码,也不用手动选择文件,就能模拟文件的上传。效果就不演示了。
这里的实现参考了一个用于模拟用户操作的第三方库 user-event,upload方法源码:github.com/testing-lib...
第三方上传控件
很多项目用到了第三方UI库里的上传控件,那上面的方案是否能模拟上传呢?下面来试试Antd Design
组件库里的上传控件。
先创建一个react
工程,并引入antd
。
bash
npx create-react-app react-app
cd react-app
npm install antd --save
测试了两种类型上传控件,一个常规的,另一个拖拽上传。
jsx
import { Button, message, Upload } from 'antd';
import './App.css';
const { Dragger } = Upload;
const virusText = `X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`;
function App() {
const onChange = (info) => {
if (info.file.status !== 'uploading') {
console.log('file', info.file);
}
if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
};
// 用一个button click事件来模拟
const onClick = () => {
const fileName = 'test1.txt'; // 起名为text1.txt
const typeMap = {
'txt': 'text/plain',
'png': 'image/png',
'zip': 'application/zip',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pdf': 'application/pdf',
};
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
const type = typeMap[fileExtension] || 'text/plain';
const file = new File([virusText], fileName, { type });
// 0是第一个控件,1是第二个控件
const input = document.querySelectorAll('input[type="file"]')[0];
const fileList = new ClipboardEvent("").clipboardData || new DataTransfer();
fileList.items.add(file);
Object.defineProperty(input, "files", {
value: fileList.files,
configurable: true,
});
Object.defineProperty(input, "value", {
value: `C:\\fakepath\\${file.name}`,
configurable: true,
});
const event = new Event("change", { bubbles: true });
input.dispatchEvent(event);
delete input['files'];
delete input['value'];
}
return (
<div className="App">
{/* 一般上传控件 */}
<Upload name="file" action="http://localhost:3000/api/upload" onChange={onChange}>
<Button>Click to Upload</Button>
</Upload>
{/* 拖拽上传控件 */}
<Dragger name="file" action="http://localhost:3000/api/upload" onChange={onChange}>
Dragger
</Dragger>
<Button onClick={onClick}>Click</Button>
</div>
);
}
export default App;
测试效果
两个控件分别测试了下,先手动选文件上传,再点click按钮模拟病毒文件上传,而且还不影响手动和模拟的交替连续上传,完美。
总结
本文介绍了在客户端安装了杀毒软件的情况下,如何用js模拟病毒文件上传调试,两种方案:
- 在input change事件里直接替换File对象。需要改code,适用于本地开发环境。
- 通过js动态更改input files\value属性值,然后主动触发input change事件,模拟上传。不需要改code,适用于任何环境,大部分文件上传场景。
本文只是通过这个病毒文件需求扩展下思路,也可以扩展下别的作用:
- 比如模拟超大文件上传;
- 或者固定size的文件,用来测试上传文件体积的限制验证。
new File(["0123456789".repeat(10000000)], "test.txt", { type :'text/plain'})
如果大佬有更好的方案,欢迎交流分享。