php://input contains data for a GET request - php

I am running Apache2 and PHP 5 on Linux, and I'm getting some strange behavior with the php://input stream.
For some GET requests the stream is not empty like it should be. Instead, the php://input stream contains the entire GET request. I have worked around the issue but I would like to know if I should file a bug about this, or if it is "desired but undocumented" behavior.
Details
Early in the request processing, I call:
$in = file_get_contents('php://input');
if ( !empty($in) )
$post_data = json_decode($in);
if ( !empty($in) && is_null($post_data) ) {
// output some error info and exit
}
Usually when a request does not have a body then $in is empty and all is right with the world. But sometimes a GET request will have a body, and that body will be the entire request. Of course you can't json-decode that data, and the error condition gets hit.
This only happens with some requests. For example, this request does not exhibit the error:
GET /os/invitations/kkkkkk HTTP/1.1
Host: our.machine.com
Content-Type: application/json
Authorization: Basic aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==
But this request, which is routed through some proxies and VPNs, does trigger the error.
GET http://some.proxy.at.some.big.company.com:7080/cvp-out/cmmproxy/os/invitations/d66065566dba541c8ba6a70329684645 HTTP/1.1
Content-Type: application/json
Authorization: Basic aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==
Clientid: abc
User-Agent: Java/1.6.0
Host: some.proxy.at.some.big.company.com:7080
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
X-Remote-Addr: 53.231.244.171
X-Remote-Host: 53.231.244.171
X-Server-Name: some.proxy.at.some.big.company.com
X-Server-Port: 7080
X-Scheme: http
I spent hours treating this like a routing/dispatch problem, but it turned out to be our code. The fix was, of course, to only read from the input stream when you are expecting data:
if ( in_array( $_SERVER['REQUEST_METHOD'], array('PUT', 'POST') )) {
$in = file_get_contents('php://input');
if ( !empty($in) )
$post_data = json_decode($in);
}
Is this a known issue? Does it happen unpredictably? Should I file a bug?

As far as i know, that's not an error. We understand that a GET request shouldnt have a body, but in the docs of php:// they say nothing about wich types of requests will generate an input, so it could be any method. And for sure it is not limited to POST, since the mention at least PUT and PROPFIND.
So at any rate, your solution is a must.

Related

How to send raw data with curl GET in PHP?

I am developing REST API and while it is easy to set raw JSON data for request in cURL for POST
$payload = json_encode(array("user" => $data));
//attach encoded JSON string to the POST fields
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
I cannot figure out how to send such data with GET requests.
Is there something like CURLOPT_GETFIELDS or CURLOPT_RAWDATA? The purpose of sending JSON with GET request is to pass in some params.
I do not wish to add formdata to the request, I wish to post JSON so that it can be parsed on the receiver.
Thanks!
EDIT:
based on comments I want to avoid confusion, so the resulting request should look like:
GET / HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Accept: application/json
Host: 127.0.0.1:3000
content-length: 13
Connection: keep-alive
cache-control: no-cache
{
"a": "b"
}
as you can see, GET request here has data and it is parsed and works perfectly by web server. How do I achieve this with cURL?
GET requests do not have a body, that's the whole idea: you're just getting something from the server, as opposed to posting something to it. From RFC 7231:
A payload within a GET request message has no defined semantics;
sending a payload body on a GET request might cause some existing
implementations to reject the request.
In other words, a GET request can have data, but it should not. From earlier in the spec, where GET is defined as a safe method:
Request methods are considered "safe" if their defined semantics are
essentially read-only; i.e., the client does not request, and does
not expect, any state change on the origin server as a result of
applying a safe method to a target resource.
...
Of the request methods defined by this specification, the GET, HEAD,
OPTIONS, and TRACE methods are defined to be safe.
If you really want to have JSON in your GET request (and send it to a reasonably implemented server resource) the only place it can go is in the URI as part of the query string. For GET requests I find using file_get_contents to be much easier than dealing with cURL.
<?php
$payload = json_encode(["user" => $data]);
$url_data = http_build_query([
"json" => $payload
]);
$url = "https://some.example/endpoint.php?" . $url_data;
$result = file_get_contents($url);
If you want to send it to an unreasonably implemented server resource, and violate the spirit of the HTTP RFCs, you could do this:
<?php
$url = "https://some.example/endpoint.php";
$payload = json_encode(["user" => $data]);
$ctx = stream_context_create(["http" => [
"header"=>"Content-Type: application/json",
"content"=>$payload
]]);
$result = file_get_contents($url, false, $ctx);
If you're determined to do this specifically with cURL, you might have luck with the CURLOPT_CUSTOMREQUEST option set to "GET" and CURLOPT_POSTDATA with your data.

PHP:: Get POST Request Content

I'm sending a POST request using WebClient.UploadData() method (C#) to my webserver. The packet sent to my webserver looks like so:
POST / HTTP/1.1
Host: {ip}
Content-Length: {length}
Expect: 100-continue
Connection: Keep-Alive
{buffer_content}
As the {buffer_content} is nowhere assigned in the $_POST array, I have the following question...
Question: How do I read the {buffer_content} with PHP?
I've stumbled upon file_get_contents('php://input'), but I'm unsure whether that is recommended to do.
Use the php://input stream:
$requestBody = file_get_contents('php://input');
This is the recommended way to do this and, in PHP 7.0, the only way. Previously, there was sometimes a global variable called $HTTP_RAW_POST_DATA, but whether it existed would depend on an INI setting, and creating it hurt performance. That variable was deprecated and removed.
Beware that prior to PHP 5.6, you can only read php://input once, so make sure you store it.
Once you have your body, you can then decode it from JSON or whatever, if you need that:
$requestBody = json_decode($requestBody) or die("Could not decode JSON");

How do I check if a specific return type is accepted by the client?

I'm building an API, and I'd like to let my clients specify the content formats they accept in my responses (xml, json, etc).
I'd also like to do it using the Accept section of the request.
Somewhere in my code, I call the following line:
$request->getAcceptableContentTypes();
which returns
array (size=5)
0 => string 'text/html' (length=9)
1 => string 'application/xhtml+xml' (length=21)
2 => string 'image/webp' (length=10)
3 => string 'application/xml' (length=15)
4 => string '*/*' (length=3)
This specific request accepts both xml and json returns... but I'm not sure how to verify that. A XML response is allowed (both by */*, application/xhtml+xml and application/xml), but its primary MIME type would be text/xml, which is not explicitly written above. Same thing applies to application/json, which is allowed by */*, but isn't explictly written.
Should I map all the possible mimetypes to its equivalents, should I enforce a specific and explict mimetype definition on my client or is there a more elegant way to do that?
EDIT: For clarification: I get Accepts: application/xhtml+xml. I'm only prepared to answer with Content-Type: text/xml. Should I throw an exception? Or should I explode the second part (xhtml+xml) and return text/xml anyway, due to the similarity of what's accepted and what I can answer? If so, how do I know they're "similar"? Will I need a lookup table? Can I disregard the first part (application)?
This process is called content negotiation.
The representation (html, json or xml) returned by a API web service is determined by the URL or by the Accept HTTP header.
Let's start to view at this from client-side:
GET http://api.company.com/products/10 HTTP/1.1
Host: http://api.company.com
Accept: application/json, text/javascript, */*; q=0.01
The server responds with
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: ...
Connection: Close
{"Id":1,"ProductName":"Helicopter","Category":"Aircraft","Price":5000}
Question: How do i check, if a specific return type is accepted by the client?"
Answer: by evaluating the accept header send by the client.
In the GET-request the client defines multiple accept headers (in order) (with preference q). The Accept-Header lists the mime-types that the client is willing to accept.
The tasks of the server would be to look at the available representations it has and serve the one with the highest preference to the client.
You have to evaluate the "Accept"-headers and the "q"-weight and send an appropriate representation to the client, indicating the type in the Content-Type - header.
The "q" argument in the Accept header is float between 0 and 1. It's a weight, indicating the preference for that media type. A higher number inidicates a higher preference.
The offical term is "relative quality factor".
The RFC describes several examples of how the preference should be understand.
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
It's not easy. Some of these things are "overlapping".
Keep it easy. Focus on "text/html", "application/json", "application/xml".
How can one implement that on server side?
It is a good practice to have a Negotiation class.
Let's say: the client requests a valid resource, but with unknown Accept header.
GET http://api.company.com/products/10 HTTP/1.1
Host: http://api.company.com
Accept: application/karamba
The request comes to the server.
The resource /product/10 is found - it's valid. The server has data.
Now "application/karamba" arrives, but there is not content-formatter for that.
The server can't deliver the format the client wants it's data in.
The server has to send an error 406 (Not Acceptable) as Response.
The workflow is roughly like this:
Request Object with a method preferedResponseType() to evaluate the Accept Header
or like you have it already $request->getAcceptableContentTypes()
good lib: http://williamdurand.fr/Negotiation/
instantiate an Response object, based on that type.
you might do that by mapping part of the string from the accept header to a class.
JsonResponse
XmlResponse
HtmlResponse
if, there isn't a response object for this type (it's unknown)
send an ErrorResponse. EXIT
fetch data from db
insert db results into response object, maybe JsonResponse()
JsonResponse()->flush() send it...
The essential part is the mapping from Accept Header to a Class which can answer that request.
Finally, you have to make a decision, if you really want to support many Response types.
Ok, so much theory. Let's take a look at a live API: GitHub.
Request to https://api.github.com/users/caspyin
browser based GET request to the API endpoint
with Accept Header text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Response is JSON send with Content-Type:application/json; charset=utf-8
Wait. What? Why?
Github responses are JSON, because they decided to keep it simple.
They expose their accepted mime-types here: https://developer.github.com/v3/media/#request-specific-version
Update (2014-09-29)
Check out this new library: https://github.com/auraphp/Aura.Accept
Especially: https://github.com/auraphp/Aura.Accept/blob/master/tests/unit/src/MediaTest.php
You need to do pattern matching not equality checking:
application/xml+json != application/json
application/xml+json != application/xml
But application/json matches application/xml+json
One thing you could do is flatten accept headers with + and do consecutive matching if only there is no */* till you find an acceptable format else return 406 Not Acceptable.
<?php
$accept = [
'text/html',
'application/json+xml',
'image/webp',
'application/xml',
'*/*'
];
function flattenAccept($accept) {
$newAccpet = [];
foreach ($accept as $mimeType) {
if (strpos($mimeType, '+') !== false) { // Contains +
$arr = explode('/', $mimeType);
$type = $arr[0];
$medias = explode('+', $arr[1]);
foreach ($medias as $media) {
array_push($newAccpet, $type."/".$media); // Flatten
}
} else {
array_push($newAccpet, $mimeType);
}
}
return array_unique($newAccpet);
}
function matching($mimeType, $accept) {
if (in_array("*/*", $accept)) return true;
return in_array($mimeType, $accept);
}
$newAccept = flattenAccept($accept);
var_dump($newAccept);
var_dump(matching("application/json", $newAccept));
Result
array(5) {
[0] =>
string(9) "text/html"
[1] =>
string(16) "application/json"
[2] =>
string(15) "application/xml"
[3] =>
string(10) "image/webp"
[5] =>
string(3) "*/*"
}
bool(true)
Most APIs which allow multiple MIME type responses force the client to specify the response format. These are usually done in one of 2 ways: either as a query parameter ('http://exampleAPI.com/getAllUsers?json') or as a request header ('Accept: application/json').
Requiring this from the client will give you explicit values for the accepted MIME types and you can decide if you'd like to respond with an error or apply a default setting when it's missing or incorrect.
This solution won't give your client an overhead either, as in a generic client-side implementation, you would create a wrapper class to automate authentication and maybe to organise your requests and post-process your responses in certain cases.
For future reference, I'd like to point out an alternative and less expensive way to accomplish what I asked on this question.
Hoa/Mime is a library that does just what I asked. It had not been releasead at the time of the question.
print_r(Hoa\Mime\Mime::getExtensionsFromMime('text/html')); will print
Array
(
[0] => html
[1] => htm
)
Similarly, var_dump(Hoa\Mime\Mime::getMimeFromExtension('webm')); will print string(10) "video/webm".
A Mime object also has the methods getExtension() and getOtherExtensions(), so I can check for alternative acceptable content-types, without coming up with my own lookup table.
This is a little more complete than #Issam Zoli's answer, as it provides room for returning, for example, text/xml when an application/xml is requested. His answer is great for a lower-level understanding of what must be done, if one wishes to implement on his own, instead of using a third-party bundle.

PHP seems to fail to parse into $_COOKIE correctly

I have an incoming HTTP request that looks like this (standard stuff with a couple of cookies being returned from the client):
GET /loggedin.php HTTP/1.1
Cookie: name=Server+Side+Name; path=/
Cookie: role=Role+From+DB; path=/
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.1; Build/JRO03C)
Host: www.example.com
Connection: Keep-Alive
Accept-Encoding: gzip
Some PHP which reads:
<?php
$body = "\nThe request's _SERVER['HTTP_COOKIE'] is: " . print_r($_SERVER['HTTP_COOKIE'], true);
$body .= "\nThe request's _COOKIE is: " . print_r($_COOKIE, true);
echo $body;
?>
Which results in the following output:
The request's _SERVER['HTTP_COOKIE'] is: name=Server+Side+Name; path=/, role=Role+From+DB; path=/
The request's _COOKIE is: Array
(
[name] => Server Side Name
[path] => /, role=Role From DB
)
Note the second entry in $_COOKIE is "path" not "role" and its value is incorrect.
It certainly looks like the cookies which appear correctly (albeit concatenated) in $_SERVER['HTTP_COOKIE'] are being parsed incorrectly to populate $_COOKIE but I can't believe that's the case. I control all of the elements so what should I be doing differently to get the correct values into $_COOKIE?
If it matters this is on PHP 5.3.27 on an EC2 Amazon Linux instance.
The answer is: it's easy to confuse the Set-Cookie and Cookie headers.
Only the Set-Cookie header has options like path and expires. The Cookie header does not because those options are consumed by the client when deciding whether to send the cookie or not.
The reason my request contains these options on the Cookie header is that I'm trying to work around an issue with certain Android HTTP implementations and got the headers confused in my code logic.

Is there any way to configure php to always set $_SERVER['CONTENT_LENGTH']?

I'm working on carddav client. As server i use davical v. 0.9.9.6. I don't understand why i'm getting invalid content-type error when http headers contains correct value. I look into source code and found this condition:
if ( isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > 7) {...
After little research I found php set $_SERVER['CONTENT_LENGTH'] only with POST method and uploading file. Is there any way to configure php to always set $_SERVER['CONTENT_LENGTH']?
I'm asking generally, not only for this case...
//EDIT
I'm doing HTTP PUT request to davical server (using php curl).
PUT /caldav.php/testuser/contacts/newc.vcf HTTP/1.1
Host: davical
Content-Type: text/vcard;
BEGIN:VCARD
VERSION:3.0
FN:ME
...
On davical side is condition testing CONTENT_LENGTH which is not set. So it's a davical bug?
//EDIT 2
Finally I figure it out!
PUT request with calback readfunc requires set INFILE_SIZE via curl_setopt(...)
There is none auto value and put Content-Length field manualy into header is also wrong.
Example (incorrect):
// PUT REQUEST
curl_setopt($ch,CURLOPT_HTTPHEADER,"Content-Length: $length"); //mistake
curl_setopt($ch,CURLOPT_PUT,true);
curl_setopt($ch,CURLOPT_READFUNCTION,array($this,'readfunc'));
....
--------------------------------------------------------------
// WIRESHARK TCP STREAM DUMP
PUT /caldav.php/testuser/contacts/novy.vcf HTTP/1.1
Authorization: Basic xxxxxxxxxxxxxxx
Host: davical
Accept: */*
Content-Type: text/vcard
Content-Length: xxx
Expect: 100-continue
HTTP/1.1 100 Continue
155
BEGIN:VCARD
VERSION:3.0
...
END:VCARD
0
HTTP/1.1 200 OK
----------------------------------------------------------------
// On server side
isset($_SERVER['CONTENT_LENGTH'])==false
Second (correct) example
// PUT REQUEST
curl_setopt($ch,CURLOPT_INFILESIZE,$length);
curl_setopt($ch,CURLOPT_PUT,true);
curl_setopt($ch,CURLOPT_READFUNCTION,array($this,'readfunc'));
....
--------------------------------------------------------------
// WIRESHARK TCP STREAM DUMP
PUT /caldav.php/testuser/contacts/novy.vcf HTTP/1.1
Authorization: Basic xxxxxxxxxxxxxxx
Host: davical
Accept: */*
Content-Type: text/vcard
Content-Length: xxx
Expect: 100-continue
HTTP/1.1 100 Continue
BEGIN:VCARD
VERSION:3.0
...
END:VCARD
HTTP/1.1 200 OK
----------------------------------------------------------------
// On server side
isset($_SERVER['CONTENT_LENGTH'])==true
Although i have never used CONTENT_LENGHT i can tell you why this is probably happening:
In a request, you don't have to set the Content-Lenght header... IT IS NOT MANDATORY. Except for specific situations. If your POSTed content is of type "multipart/form-data" it becomes necessary to use content-lenght for each part because each part is seperated by a boundary and each part will have its own headers...
For example:
Content-Type: MultiPart/Form-Data
Boundary: #FGJ4823024562DGGRT3455
MyData=1&Username=Blabla&Password=Blue
#FGJ4823024562DGGRT3455==
Content-Type: image/jpef:base64
Content-Lenght: 256
HNSIFRTGNOHVDFNSIAH$5346twSADVni56hntgsIGHFNR$Iasdf==
So here this is a crude example of what a multi part request works, you see that the second part has a content-lenght. This is why sometimes the content-lenght is set and sometimes not, because you need to read X bytes before finding another boundary and extract the correct data.
It doesn't mean your server will never send it in in other cases, but my 2 cents are this is the case right now. Its because you are not in POST, but in some other modes.
Only requests that have a request body have a content length request header (or at least only then it makes sense) and so therefore the $_SERVER variable is set.
If you need it to be always set (which I think is bogus), you can do this yourself on the very beginning of your script:
isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] = 0;
Assuming that if it is not set, it's of zero length. See as well Improved handling of HTTP requests in PHP.
You could probably set them by yourself. Why do you need that this values are set? And what should they set to?
Maybe you're missing information on $_SERVER['CONTENT_TYPE'] or
$_SERVER['CONTENT_LENGTH'] as I did. On POST-requests these are
available in addition to those listed above.
-> http://www.php.net/manual/en/reserved.variables.server.php#86495

Categories