C/C++ CGI处理文件上传
前端页面如下:(index.html)
<!DOCTYPE>
<html>
<head>
<title>Upload File Test</title>
</head>
<body>
<form enctype='multipart/form-data' action="/cgi-bin/LocalVideoUpload" method="post">
<input type="file" name='pics' multiple>
<input type="file" name='another_pic'>
<input type="submit">
</form >
</body>
</html>
从中可以看到,使用了一个表单作为上传内容,其中enctype属性表明这个表单在上传的时候使用multipart/form-data格式。表单中有两个输入框,在浏览器里会显示为【选择文件】。其中pics可以选择多个文件上传,another_pic只能选择一个文件上传。
那么什么是multipart/form-data格式呢?
首先根据标准文档和搜索到的资料,这是一种Content-Type,而且是建立于POST请求方式基础上的。其本身意味着整个表单数据编码为一条消息,每个控件的数据对应消息的一部分。
先用Wireshark抓一下包,这里要注意的是,由于wireshark没法抓localhost环回的包,因此我在本地的另一个设备上设置了一个代理,这样请求会经过该设备的中转,从而能够被wireshark捕获到。当然也有更好的办法,比如配置路由,安装rawpacp等等.... 可以参考 这个博客
抓到的包大概是这样:
这里用了wireshark的过滤条件:ip.src eq 192.168.31.102 and ip.dst eq 192.168.31.51 and ip.proto eq TCP,这样能够筛选出从102机器发往51机器的所有TCP报文,沿着SYN查找带有PSH,ACK的报文(其中PSH表示push操作位,意味着发送方希望接收方立刻处理数据,具体参见 这个博客 ,在截图中还可以看到很多TCP segment of a reassembled PDU这样的提示,这表明TCP报文经过了分片,可以参考 这个博客。关于更多的wireshark过滤条件语法,参见 这个博客)然后右键报文——追踪TCP流,就会弹出来这样一个窗口:
这里就可以看到报文的格式了。在Content-Type中,multipart/form-data被指定,随后跟着一个boundary参数顾名思义就是消息的边界分隔符。在POST的正文中可以看到,每个消息以--{$boundary}开始。最后一个消息后附带一个--{$boundary}--表示结束。整个POST的正文长度(包括一大堆的分隔符和内部消息头)为Content-Length。
这样,在CGI中,接收到CONTENT_TYPE和CONTENT_LENGTH参数后,分析是不是multipart/form-data,如果是就提取出来boundary,读取post内容,根据boundary分割输入数据,解析内部请求头。(此处代码较多,就不贴了)
需要注意的是,如果CGI运行在Windows下,而且上传的文件中包含二进制数据,此时需要将标准输入流设置成二进制模式,具体参见 这个博客。
有关内部消息头中的Content-Disposition,可以参考 这里。另外Content-Disposition还可以被用在响应头中,用于下载文件,具体参考 这个博客。
由于内部消息头没有Content-Length,因此没有办法预知这一部分有多少数据,只能不断读入然后判断是否遇到了边界。
需要小心的是,内部消息头指出的filename可能是经过精心编辑的带有类似 ../ 的路径,如果直接使用有可能会覆盖其他文件,从而为攻击提供了可乘之机。
这里再介绍一种能够显示上传进度的前端页面写法:(摘自 这个博客)
<!DOCTYPE html>
<html>
<head>
<title>Upload Files using XMLHttpRequest - Minimal</title>
<script type="text/javascript">
function fileSelected() {
var file = document.getElementById('fileToUpload').files[0];
if (file) {
var fileSize = 0;
if (file.size > 1024 * 1024)
fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB';
else
fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB';
document.getElementById('fileName').innerHTML = 'Name: ' + file.name;
document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize;
document.getElementById('fileType').innerHTML = 'Type: ' + file.type;
}
}
function uploadFile() {
var fd = new FormData();
fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]);
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", uploadComplete, false);
xhr.addEventListener("error", uploadFailed, false);
xhr.addEventListener("abort", uploadCanceled, false);
xhr.open("POST", "/cgi-bin/LocalVideoUpload");//修改成自己的接口
xhr.send(fd);
}
function uploadProgress(evt) {
if (evt.lengthComputable) {
var percentComplete = Math.round(evt.loaded * 100 / evt.total);
document.getElementById('progressNumber').innerHTML = percentComplete.toString() + '%';
}
else {
document.getElementById('progressNumber').innerHTML = 'unable to compute';
}
}
function uploadComplete(evt) {
/* 服务器端返回响应时候触发event事件*/
alert(evt.target.responseText);
}
function uploadFailed(evt) {
alert("There was an error attempting to upload the file.");
}
function uploadCanceled(evt) {
alert("The upload has been canceled by the user or the browser dropped the connection.");
}
</script>
</head>
<body>
<form id="form1" enctype="multipart/form-data" method="post">
<div class="row">
<label for="fileToUpload">Select a File to Upload</label><br />
<input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected();"/>
</div>
<div id="fileName"></div>
<div id="fileSize"></div>
<div id="fileType"></div>
<div class="row">
<input type="button" onclick="uploadFile()" value="Upload" />
</div>
<div id="progressNumber"></div>
</form>
</body>
</html>
此外,还可以使用libcurl库和winhttp库来直接对cgi发起上传文件的POST请求,可以参考 这个博客。
POST上传数据的时候,除了multipart/form-data外,还有默认的application/x-www-from-urlencoded类型,这种类型会将数据转化为键值对key=value&key2=value2这样的,同时还会对数据进行编码(不适合文件上传)。还有raw类型,其Content-Type为text/plain或者application/json,text/html这样的。其内容不会被修改。有关更多的Content-Type内容,可以参考 这个博客。除此之外还有一种binary类型,其Content-Type为application/octet-stream,由于没有分隔符,因此只能上传一个文件。