phphttphttp-headersfsockopen

HTTP header "Content-type: multipart/mixed" causes "400 Bad request"


I'm trying to upload a file to a remote server using php's fsockopen. If my request looks like:

$httpContent = [
        "POST /index.php HTTP/1.1", 
        "Host: The IP address of my remote server", 
        "Connection: Close", 
        "User-Agent: My client"
];

the server responses with 200 OK. However if I add

"Content-Type: multipart/mixed; boundary=".md5('myBoundary'), 
"--".md5('myBoundary'), 
"Content-Type: text/plain", 
"value", 
"--".md5('myBoundary')."--"

The server returns "400 Bad Request". Everything seems fine to me, but somehow it doesn't work. What am I doing wrong?

Any help is greatly appreciated!

Edit (@Rei advised to post a dump of the request):

this is what echoes in client's terminal

POST /index.php HTTP/1.1
Host: The IP address of my remote server
Connection: Close
User-Agent: My client
Content-Type: multipart/mixed; boundary=be0850c82dd4983ddc49a51a797dce49
--be0850c82dd4983ddc49a51a797dce49
Content-Type: text/plain
value
--be0850c82dd4983ddc49a51a797dce49--

And this is what the server gets (caught with tcpdump):

POST /index.php HTTP/1.1
Host: The IP address of my remote server
Connection: Close
User-Agent: My client
Content-Type: multipart/mixed; boundary=be0850c82dd4983ddc49a51a797dce49
--be0850c82dd4983ddc49a51a797dce49
Content-Type: text/plain
value
--be0850c82dd4983ddc49a51a797dce49--

I personally don't see any difference between the two and also don't see anything wrong with the request. Yet, the server returns "400 Bad Request".

*Note: The "The IP address of my remote server" is a real IP address, but I'm not posting it due to security concerns.

**Note: Didn't mention that the PHP script is run in a CLI environment and is not used as a web-application back-end (the requests are NOT prepared by a web browser).


Solution

  • Just as I suspected, the HTTP request you're sending does not follow HTTP specification. See RFC7230 Section 3. The problem: there are missing CRLFs.

    There should be an extra CRLF after the last HTTP header, and one after the last MIME header, too.

    POST /index.php HTTP/1.1
    Host: The IP address of my remote server
    Connection: Close
    User-Agent: My client
    Content-Type: multipart/mixed; boundary=be0850c82dd4983ddc49a51a797dce49
    
    --be0850c82dd4983ddc49a51a797dce49
    Content-Type: text/plain
    
    value
    --be0850c82dd4983ddc49a51a797dce49--
    

    That will fix the "Bad Request" problem.

    Although the cause of the problem is not the Content-Type: multipart/mixed header, you should know that it is deprecated. Use Content-Type: multipart/form-data instead. See related answer here and RFC7578.

    Example of correct request

    The content type multipart/form-data can be used to send values and files. The difference is only the attribute filename.

    Modifying your HTTP request as little as possible, here is an example that sends two fields (one file and one value):

    POST /index.php HTTP/1.1
    Host: The IP address of my remote server
    Connection: Close
    User-Agent: My client
    Content-Type: multipart/form-data; boundary=be0850c82dd4983ddc49a51a797dce49
    Content-Length: 234
    
    --be0850c82dd4983ddc49a51a797dce49
    Content-Disposition: form-data; name="one"; filename="example.txt"
    
    foo
    --be0850c82dd4983ddc49a51a797dce49
    Content-Disposition: form-data; name="two"
    
    bar
    --be0850c82dd4983ddc49a51a797dce49--
    

    For each field, you need a Content-Disposition header to be able to name the field and optionally give it a filename.

    Content-Type is optional although you may want to add one if the value comes garbled.

    Modify as necessary if you only need to upload one file.

    Testing the request

    Here's a neat little script that I use to test HTTP requests:

    <?php
    header('Content-Type: application/json');
    echo json_encode([
        'method' => $_SERVER['REQUEST_METHOD'],
        'uri' => $_SERVER['REQUEST_URI'],
        'body' => file_get_contents('php://input'),
        'GET' => $_GET,
        'POST' => $_POST,
        'FILES' => $_FILES,
        'headers' => getallheaders(),
    ], JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
    

    With that script at the receiving end, I sent my example request without modification to my localhost server and I got this result:

    HTTP/1.1 200 OK
    Date: Tue, 31 Jul 2018 11:33:57 GMT
    Server: Apache/2.4.9 (Win32)
    Connection: close
    Transfer-Encoding: chunked
    Content-Type: application/json
    
    254
    {
        "method": "POST",
        "uri": "/index.php",
        "body": "",
        "GET": [],
        "POST": {
            "two": "bar"
        },
        "FILES": {
            "one": {
                "name": "example.txt",
                "type": "",
                "tmp_name": "C:\\wamp\\tmp\\phpD6B5.tmp",
                "error": 0,
                "size": 3
            }
        },
        "headers": {
            "Host": "The IP address of my remote server",
            "Connection": "Close",
            "User-Agent": "My client",
            "Content-Type": "multipart/form-data; boundary=be0850c82dd4983ddc49a51a797dce49",
            "Content-Length": "234"
        }
    }
    0
    

    As you can see, field "one" goes to $_FILES and field "two" goes to $_POST.

    First, send the example request without modification. Keep trying until you get the correct result. Then modify as needed, making sure every step of the way that the request still follows HTTP specification.