I want to process a post request which sends a file. However the CGI script written in bash on an Apache server fails to upload the file to the server. I was able to pin the error down to /dev/stdin
not working as it's supposed to. Instead of writing the binary stream out it throws an error. I'm working with the light-weight open source content management system Lichen and the reason why I originally encountered this problem.
In the following, I will first simplify the problem and show all the things I did wrong trying to upload a file from a html-front-end with a CGI back-end using shell script.
Simplifying the problem
The html post form to upload files looks like this:
<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>
It sends its request to 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 ''
However $PATH_INFO
won't hold a string of the filename that's in the process of being uploaded as we see later, so the upload fails?
On the other hand in the production environment, a new file is created with the correct filename, which can be seen in the correct directory, however the file is empty. :o
I'm wondering how it done that?
A test.cgi script to validate if all data in test environment is send accordingly and to proof that PATH_INFO is empty:
#!/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>"
Has following output when client runs it through the html file post form:
> 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ªµù¦
The last bit is intentionally cut off so to not show 80kb of file and it's what the command dd count=1 bs=$CONTENT_LENGTH
prints. It does a great job at printing out the content of the uploaded file (just not correctly encoded), proving that it somehow works. However, the file-content is never saved on the server.
With that we were able to confirm that the upload.cgi script receives the file in our test environment, although not the PATH_INFO
and thus the cgi-script fails?
Also this is Apache's error message: Premature end of script headers: upload.cgi
and the apache error_log at /var/log/httpd/error_log
shows following error_code:
> 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/
Pointing towards line 20 in the upload.cgi script (for full context look for the script above), although I guess it actually means line 19:
mkdir -p ..$(dirname $sanitized)
Where dirname
fails on its argument since $sanitized:
sanitized=$(echo $PATH_INFO | tr -s ' ' '_')
is actually an empty string, since $PATH_INFO holds no value! - as we have seen.
I greatly appreciate any help and will be very happy if there is a solution to this problem. :) The goal is to have the file correctly uploaded on the server.