shell 通过CGI脚本通过Bash从HTML输入上传文件

3bygqnnd  于 2023-03-09  发布在  Shell
关注(0)|答案(2)|浏览(254)

我想处理一个发送文件的post请求。但是在Apache服务器上用bash编写的CGI脚本无法将文件上传到服务器。我可以将错误归结为/dev/stdin没有按预期工作。它没有写出二进制流,而是抛出了一个错误。我正在使用轻型-权重开源内容管理系统Lichen和我最初遇到这个问题的原因。
在下面,我将首先简化这个问题,并展示我在尝试使用shell脚本从HTML前端和CGI后端上传文件时所做的所有错误。

简化问题

用于上传文件的html post表单如下所示:

<form action="http://guestserver/cgi-bin/upload.cgi" method="post" enctype="multipart/form-data">
    <p><input type="file" name="filename" id="file"></p>
    <p><input type="submit" value="Upload"></p>
</form>

它将其请求发送到upload.cgi

#!/bin/sh

#  Exit immediately if a command exits with a non-zero status.
set -e

# replace spaces with underscores
#   which is done by tr "thisGetsReplaced" "byThis" ;
#   -s means squeeze repeated occurence
#   echo $PATH_INFO | 
#   gets filename and passes output to next (by '|')
sanitized=$(echo $PATH_INFO | tr -s ' ' '_')

# move one dir up and look if file exists there
if [ -f ..$sanitized ]; then
    cat /dev/stdin > /dev/null
    echo 'Status: 409 Conflict'
    echo 'Content-Type: text/plain'
    echo ''
    echo 'File already exists.'
    exit 0
fi

# Actual file write
mkdir -p ..$(dirname $sanitized)
cat /dev/stdin > ..$sanitized # line that throws error

# I guess if file write at this point was successful 
#        it exits with something non-zero
#        so the script is STOPPED
echo 'Status: 204 No Content'
echo "X-File-Name: $(basename $sanitized)"
echo "X-File-Path: $(dirname $sanitized)"
echo ''

但是$PATH_INFO不会保存正在上传的文件名字符串,我们稍后会看到,因此上传失败。另一方面,在生产环境中,会创建一个具有正确文件名的新文件,该文件名可以在正确的目录中看到,但是该文件为空。:o
我想知道它是怎么做到的?
一个test.cgi脚本,用于验证测试环境中的所有数据是否都已相应发送,并证明PATH_INFO为空:

#!/bin/sh

echo "Content-Type: text/html"
echo "<html><head></head><body>"

echo SERVER_<?> = $SERVER_<?> # just so I don't have to write so much
echo PATH_INFO = $PATH_INFO

dd count=1 bs=$CONTENT_LENGTH # prints file content

echo "</body></html>"

当客户端通过html文件发布表单运行时,具有以下输出:

> SERVER_SOFTWARE = Apache/2.4.55 (Unix)
> GATEWAY_INTERFACE = CGI/1.1
> SERVER_PROTOCOL = HTTP/1.1
> SERVER_PORT = 80
> SERVER_PROTOCOL = HTTP/1.1
> SERVER_PORT = 80
> REQUEST_METHOD = POST
> HTTP_ACCEPT =
> PATH_INFO =
> \------WebKitFormBoundaryMNBsYvUe3DbH9tpE Content-Disposition: form-data; name="filename"; filename="uploaded file.jpg" Content-Type: image/jpeg ÿØÿàJFIFÿÛC  %# , #&')\*)-0-(0%()(ÿÀ,àÿÄÿÄ; !"#$312%4CB“5ADQRcdƒabe„”•ÿÚ?•ñ£Ö˜þFßE%ò}õ\[/ì³è1Æ'¬YÇ­çªÙæÞõÑTÚuJn4îÝ)ÎV“¦­9îª ©“1í”»ge¢R…Z¿MÑŽ¼ÜÃÛ—d´¯±¦#ø4¦‚ðœDÐŽæ…c4û°e¥4ê×1žOO qu»Ö:ûïAB¬?ÙܶbZÎf³ª‹¹yçDÖÒáSªµù¦

最后一位被故意剪掉了,这样就不会显示80kb的文件,而这正是dd count=1 bs=$CONTENT_LENGTH命令打印的内容。它在打印上传文件的内容(只是编码不正确)方面做得很好,证明它以某种方式工作。然而,文件内容从未保存在服务器上。
这样我们就可以确认upload. cgi脚本在我们的测试环境中接收到文件,尽管PATH_INFO没有接收到文件,因此cgi脚本失败了。
这也是Apache的错误消息:Premature end of script headers: upload.cgi/var/log/httpd/error_log处的apache错误日志显示以下错误代码:

> dirname: missing operand
> Try 'dirname --help' for more information.
> /srv/http/cgi-bin/upload.cgi: line 20: ..: Is a directory
> \[Mon Mar 06 12:29:04.166828 2023\] \[cgid:error\] \[pid 297:tid 140340891719360\] \[client 192.168.56.1:63168\] Premature end of script headers: upload.cgi, referer: http://localhost:5500/

指向upload.cgi脚本中的第20行(完整的上下文请查看上面的脚本),尽管我猜它实际上指的是第19行:
mkdir-p ..$(已清理的目录名$)
其中,dirname在$sanitized之后在其参数上失败:
已清理=$(回显$PATH_INFO|tr-s """_")
实际上是一个空字符串,因为$PATH_INFO没有值! -正如我们所看到的。
我非常感谢任何帮助,并将非常高兴,如果有一个解决这个问题。:)的目标是有文件正确上传到服务器上。

pnwntuvh

pnwntuvh1#

如果你是来修复你的Lichen upload.cgi脚本的,请跳到最后一部分,我们在这里修复了/dev/stdin。前两部分是解决我在搜索真正的错误时(最后一部分)所创建的错误。

路径信息

冷静点,一切都在正常运转。
你的第一个错误是PATH_INFO从url中取值,所以你的post-request实际上应该包含文件名,就像这个例子一样,例如http://server/cgi-bin/test.cgi/thisWillBePassedTo-PATH_INFO.file
所以你的简化问题是错误的,这就解释了为什么你仍然可以在你的生产环境中上传文件,但是没有任何内容(我们将在第三部分中看到),因为名称在PATH_INFO中被正确地传输,这给你带来了很多困惑。
下一次请查看the docs

正确设置标题

关于错误:Premature end of script headers
虽然互联网提供了十几种设置CGI脚本头的方法,但长时间的试验和错误过程表明,正确的头只发送文本看起来像这样:

echo 'content-type: text/plain'
echo ''
echo 'Hello World'

大写字母还是小写字母并不重要,但是空的echo ''(设置换行符)非常重要!
此外,您还可以发送状态头,它们看起来会有所不同(您可以在upload.cgi脚本的下一个代码块中看到它们)。

目录号:/器械/标准输入:无此类设备或地址

我试图运行静态网站内容管理系统(CMS)Lichen,当这个错误发生cat: /dev/stdin: No such device or address.
我假设服务器环境导致这个错误(我运行的是Arch/Linux)
为了解决这个问题,我不得不使用一个不同的命令来读取stdin,这个命令叫做dd
dd修改upload.cgi脚本中的两行代码就完成了:

#! /bin/sh

set -e

sanitized=$(echo $PATH_INFO | tr -s ' ' '_')

# move one dir up and look if file exists there
if [ -f ..$sanitized ]; then
        # changed this line
        dd of=/dev/null # post request has to be processed in some way!
        echo "Status: 409 Conflict"
        echo 'Content-Type: text/plain'
        echo ''
        echo "File already exists."
        exit 0
fi`enter code here`

# creating file
mkdir -p ..$(dirname $sanitized)
# changed following line
# directing binary data stream from post request to that file
dd of=..$sanitized 

# File upload successful message
echo 'Status: 204 No Content'
echo "X-File-Name: $(basename $sanitized)"
echo "X-File-Path: $(dirname $sanitized)"
echo ''

请注意,来自前端的post请求应该只发送文件的主体,否则处理数据流的CGI脚本将无法正确保存您发送的文件。更多信息请参见this post的改进部分。

7kjnsjlb

7kjnsjlb2#

使用CGI shell脚本上传文件的简单演示

    • 什么是CGI?**CGI(公共网关接口)定义了Web服务器与外部内容生成程序(通常称为CGI程序或CGI脚本)交互的方式。这是一种使用您最熟悉的编程语言将动态内容放到Web站点上的简单方法。copied from apache tutorials

CGI脚本通常位于自己的cgi-bin中,在那里它们也有执行权限,以及它们所在的文件夹。(可以用chmod o=rwx /path/to/folder-and-or-cgiFile设置,这通常是错误的来源)。此外,Apache必须进行广泛的配置,阅读上面提供的链接以了解更多信息,不要忘记重新启动Apache,否则您的更改将不会有任何效果。

服务器端

在我们的例子中,我们将使用shell脚本,它是预装在GNU/Linux机器上的。在我的例子中,我使用GNU bash,Version 5.1.16,它可以通过输入sh在命令提示符下调用。

    • 创建一个CGI脚本:**在开始时,我们定义使用什么编程语言来执行脚本。
#! /bin/sh

# Exit immediately if a command exits with a non-zero status.
set -e

# replace spaces with underscores
sanitized=$(echo $PATH_INFO | tr -s ' ' '_')

# move one dir up and look if file exists there
if [ -f ..$sanitized ]; then
        dd of=/dev/null # post request has to be processed in some way!
        # if the above isn't working you may try
        # cat /dev/stdin > /dev/null
        echo "Status: 409 Conflict"
        echo 'Content-Type: text/plain'
        echo ''
        echo "File already exists."
        exit 0
fi


# creating file
mkdir -p ..$(dirname $sanitized)
# directing binary data stream from post request to that file
dd of=..$sanitized
# if the above isn't working you may try
# cat /dev/stdin > ..$sanitized

# because front-end send file head and footer, which have to be removed
# removing first four lines
sed -i -e 1,4d ..$sanitized
# removing last line
sed -i '$d' ..$sanitized

# File upload successful message
echo 'Status: 204 No Content'
echo "X-File-Name: $(basename $sanitized)"
echo "X-File-Path: $(dirname $sanitized)"
echo 'Content-type: text/plain'
echo ''
echo 'File upload successful'

提示,分析Apache的错误日志,如果事情不工作:cat /var/log/httpd/error_log

客户端

接下来,我们需要一个前端来尝试一些东西。下面是我们的html部分:

<head>
    <script>
    function changeAction(event) {
        // only allows to upload one file at the time
        let file = (event.target.files[0])

        let filename = file.name
        // changing post action so CGI script has value for setting filename
        document.getElementById('upload').action = "http://guestserver/cgi-bin/test.cgi" + "/" + filename
        }
    </script>
</head>
<body>

<form id="upload" action="gets changed by js" method="post" enctype="multipart/form-data">
        <p><input type="file" name="filename" id="file"></p>
        <p><input type="submit" value="Upload"></p>
    </form>

    <script>
        document.getElementById('file').addEventListener('change', changeAction);
    </script>

</body>

解释

在我们的html文件中需要额外的Javascript,以提供粘贴到url的文件名,然后在后端通过$PATH_INFO访问该文件名,清理以删除任何空格,并在cgi-bin之外创建一个目录,其中dd或cat打印来自/dev/stdin的数据流。

我们的前端也会发送一些需要删除的文件头。CGI脚本会删除前四行

------WebKitFormBoundary3QyavaR9qMflXs0W
Content-Disposition: form-data; name="filename"; filename="test.txt"
Content-Type: text/plain
\n

这是一个非常古怪的解决方案。我建议多读一些关于that的文章,我不确定这个例子在这方面有多稳定。原始的源代码通过只发送主体来防止这种情况,这是用一些JavaScript完成的(如本文所示)。
这个脚本是基于CMS Lichen源代码的,但是我不得不修改一下使用"dd",否则它会失败。你可能要试试哪一个适合你。

改善

这就是使用JavaScript只发送文件体的方法,这使得CGI脚本中删除文件最后和前四行的部分变得多余:

async function uploadFile(event) {

try {
    let file = event.target.files[0];
    let filename = file.name
    const res = await fetch('http://host/url-path/to/cgi-upload-script' + '/' + filename, {
            method: 'POST',
            body: file,
        });
    if (res.status != 204) {
            const body = await res.text();
            alert('There was an error uploading the file:\n\n' + body);
            throw new Error(body);
        }

    // process upload
    const filename = res.headers.get('x-file-name');

} catch (e) {
        throw new Error(e);
    }

不要忘记更改URL。
由EvenListner调用的uploadFile函数,EvenListner必须在出现html <input type="file" id="file">时定义

<script>
 document.getElementById('file').addEventListener('change', uploadFile);
    </script>

我相信你可以进一步改进这个脚本,摆脱复杂的网址编辑和删除文件头的原因。请让我知道你是如何改进脚本的。
而且要注意upload.cgi脚本至少应该通过.htaccess来保护--我对网络安全知之甚少!

相关问题