PHP Curl - Get Response headers with duplicate keys - php

I'm having a hard time trying to parse response headers. Returned headers contain duplicate keys with the name of set-cookie which I'm trying to get along with the other headers but the problem is it overwrides the set-cookie value to the last one available. I know that we cannot have multiple name of the same key in an associative array, in that case what shuold I use to achieve this result?
[16] => set-cookie: GPS=1; Domain=.youtube.com; Expires=Sat, 14-Aug-2021 15:09:33 GMT; Path=/; Secure; HttpOnly
[17] => set-cookie: YSC=HYwhlVWBBfQ; Domain=.youtube.com; Path=/; Secure; HttpOnly; SameSite=none
[18] => set-cookie: VISITOR_INFO1_LIVE=E2nuslFFVxI; Domain=.youtube.com; Expires=Thu, 10-Feb-2022 14:39:33 GMT; Path=/; Secure; HttpOnly; SameSite=none
The script goes on forever if I include the while statement and if I remove it, it works fine but returning only the last set-cookie which is 18
$url = "https://www.youtube.com";
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, 1);
curl_setopt($curl, CURLOPT_NOBODY, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$resp = curl_exec($curl);
curl_close($curl);
$h = array_filter(explode("\n", $resp), 'trim');
$headers = [];
foreach ($h as $k => $v) {
$l = explode(": ", $v); // Desired keys values here.
if (strpos($v, 'HTTP') == true) {
$headers['Headers']['status'] = $l[0];
} elseif (strpos($v, 'set-cookie') !== false) {
// $headers['Headers']['Cookies'][]['set-cookie'] = $l[1]; Works but return each cookie as array
while (str_contains($v, 'set-cookie')) {
$headers['Headers']['set-cookie'] = $l[1];
}
} else {
$headers['Headers'][$l[0]] = $l[1];
}
}

The reason you get an array using the code below is that you are exploding the header keys using : which it is also available in the content of cookie values.
You can implode the initial exploded array from index 1 and it should work fine, so the code would be:
$headers['Headers']['Cookies'][]['set-cookie'] = implode(':', array_slice($l, 1));
Another solution is that you replace the set-cookie: with empty string and use it.
$headers['Headers']['Cookies'][]['set-cookie'] = str_replace("set-cookie:",'', $v);

Related

PHP curl - obtain response headers when error exists?

I am calling a REST service using php curl. If an error occurs (for example because I posted invalid data) the REST server returns error code 400 and provides informative application error details in the response header custom field.
However, when error 400 occurs the header is not provided in the result from curl_exec() at it returns FALSE even though setopt as been provided. Headers are seen if code returned is 2xx.
curl_setopt($curl,CURLOPT_HEADER, 1);
Is there any way to get the response headers on errors >= 400?
In the example below, I'm using https://httpstat.us/400 to simulate a HTTP 400 response code.
<?php
// create curl resource
$ch = curl_init();
// set url that responds with HTTP 400 status
curl_setopt($ch, CURLOPT_URL, "https://httpstat.us/400");
//return the transfer as a string
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//enable headers
curl_setopt($ch, CURLOPT_HEADER, 1);
//get only headers
curl_setopt($ch, CURLOPT_NOBODY, 1);
// $output contains the output string
$output = curl_exec($ch);
// close curl resource to free up system resources
curl_close($ch);
$headers = [];
$output = rtrim($output);
$data = explode("\n",$output);
$headers['status'] = $data[0];
array_shift($data);
foreach($data as $part){
//some headers will contain ":" character (Location for example), and the part after ":" will be lost, Thanks to #Emanuele
$middle = explode(":",$part,2);
//Supress warning message if $middle[1] does not exist, Thanks to #crayons
if ( !isset($middle[1]) ) { $middle[1] = null; }
$headers[trim($middle[0])] = trim($middle[1]);
}
// Print all headers as array
print_r($headers);
This returns
Array
(
[status] => HTTP/1.1 400 Bad Request
[Cache-Control] => private
[Content-Length] => 15
[Content-Type] => text/plain; charset=utf-8
[Server] => Microsoft-IIS/10.0
[X-AspNetMvc-Version] => 5.1
[Access-Control-Allow-Origin] => *
[X-AspNet-Version] => 4.0.30319
[X-Powered-By] => ASP.NET
[Set-Cookie] => ARRAffinity=93fdbab9d364704de8ef77182b4d13811344b7dd1ec45d3a9682bbd6fa154ead;Path=/;HttpOnly;Domain=httpstat.us
[Date] => Wed, 13 Nov 2019 23:31:51 GMT
)
That array with all response headers matches up with what I get when I use curl from my terminal:
curl -v https://httpstat.us/400
returns
< HTTP/1.1 400 Bad Request
< Cache-Control: private
< Content-Length: 15
< Content-Type: text/plain; charset=utf-8
< Server: Microsoft-IIS/10.0
< X-AspNetMvc-Version: 5.1
< Access-Control-Allow-Origin: *
< X-AspNet-Version: 4.0.30319
< X-Powered-By: ASP.NET
< Set-Cookie: ARRAffinity=93fdbab9d364704de8ef77182b4d13811344b7dd1ec45d3a9682bbd6fa154ead;Path=/;HttpOnly;Domain=httpstat.us
< Date: Wed, 13 Nov 2019 23:33:19 GMT
Here's another option using the CURLOPT_HEADERFUNCTION option with a callback function:
<?php
// this holds the response headers from the curl call
$responseHeaders = array();
// this function processes the response headers from the curl call
function curlResponseHeaderCallback($ch, $headerLine) {
global $responseHeaders;
// trim all the whitespace on this line
$trimmed = trim($headerLine);
// only proceed if the string is not empty
if(!empty($trimmed)) {
// headers follow Key: Value format
$split = explode(':', $trimmed);
// only proceed if the value of the header is not empty
if(!empty($split[1])) {
// $split[0] is the Key of the response header
// $split[1] is the Value of the response header, which can also have whitespace
$responseHeaders[$split[0]] = trim($split[1]);
}
}
// who knows why, but you have to return this.
return strlen($headerLine);
}
// get cURL resource
$ch = curl_init();
// set url
curl_setopt($ch, CURLOPT_URL, "https://httpstat.us/400");
curl_setopt($ch, CURLOPT_HEADERFUNCTION, "curlResponseHeaderCallback");
// send the request
curl_exec($ch);
// close the handle
curl_close($ch);
print_r($responseHeaders);
returns
Array
(
[Cache-Control] => private
[Content-Length] => 15
[Content-Type] => text/plain; charset=utf-8
[Server] => Microsoft-IIS/10.0
[X-AspNetMvc-Version] => 5.1
[Access-Control-Allow-Origin] => *
[X-AspNet-Version] => 4.0.30319
[X-Powered-By] => ASP.NET
[Set-Cookie] => ARRAffinity=93fdbab9d364704de8ef77182b4d13811344b7dd1ec45d3a9682bbd6fa154ead;Path=/;HttpOnly;Domain=httpstat.us
[Date] => Wed, 13 Nov 2019 23
)

PayPal IPN leads to 502 Proxy Error

I am using the IPN code sample (for PHP) available on Github. When running this code via the IPN simulator, there is a huge loading time and after a few minutes PayPal finally throws this error.
Proxy Error
The proxy server received an invalid response from an upstream server. The proxy server could not handle the request POST /webapps/developer/applications/ipn_simulator.
Reason: Error reading from remote server
Another user faced the same exact issue here. However, it seems his problem was due to the fact he was not using the good script.
My ipn.php file, when accessed directly in the browser does not throw any error. It looks as follows:
/* INITIALIZATION */
require_once $_SERVER['DOCUMENT_ROOT'] . '/lib/aes.php'; // AES lib
require_once $_SERVER['DOCUMENT_ROOT'] . '/lib/db.php'; // DB configuration
require_once $_SERVER['DOCUMENT_ROOT'] . '/lib/mandrill/src/Mandrill.php'; // Mandrill mailer lib
$UA = $_SERVER['HTTP_USER_AGENT']; // User-Agent
//$proxy = '<my IP>:443';
$ip = getenv('HTTP_CLIENT_IP')?:
getenv('HTTP_X_FORWARDED_FOR')?:
getenv('HTTP_X_FORWARDED')?:
getenv('HTTP_FORWARDED_FOR')?:
getenv('HTTP_FORWARDED')?:
getenv('REMOTE_ADDR'); // IP address
$paypal_business = '< MY PAYPAL ID >'; // My PayPal ID
$license_amount = array( 10 => 0,
25 => 1,
50 => 2); // Type of license wrt to amount paid
$license_types = array( 0 => 1,
1 => 5,
2 => 20); // Number of licenses wrt type of license
$license_names = array( 0 => 'Single',
1 => 'Group',
2 => 'Company'); // Name of license wrt type of license
$donated = false;
/* MAILER powered by Mandrill */
function GmSendMail($bdy, $sbj, $em) {
try {
$mandrill = new Mandrill('< MY API KEY >'); // Mandrill API Key
$message = array(
'html' => $bdy,
'subject' => $sbj,
'from_email' => 'mail#example.com',
'from_name' => 'Company',
'to' => array(
array(
'email' => $em,
'type' => 'to'
)
),
'headers' => array('Reply-To' => 'support#example.com'),
'important' => true,
'track_opens' => true,
'track_clicks' => null,
'auto_text' => null,
'auto_html' => true,
'inline_css' => null,
'url_strip_qs' => null,
'preserve_recipients' => null,
'view_content_link' => null,
'tracking_domain' => null,
'signing_domain' => 'example.com',
'return_path_domain' => null,
'merge' => true,
'tags' => array('license-activation'),
'metadata' => array('website' => 'example.com')
);
$async = false;
$ip_pool = 'Main Pool';
$result = $mandrill->messages->send($message, $async, $ip_pool);
//print_r($result);
} catch(Mandrill_Error $e) {
// Mandrill errors are thrown as exceptions
echo 'An unexpected error occurred: ' . get_class($e) . ' - ' . $e->getMessage();
// A mandrill error occurred: Mandrill_Unknown_Subaccount - No subaccount exists with the id 'customer-123'
throw $e;
}
}
/* PAYPAL IPN: CHECK PAYMENT HAS BEEN COMPLETED */
// CONFIG: Enable debug mode. This means we'll log requests into 'ipn.log' in the same directory.
// Especially useful if you encounter network errors or other intermittent problems with IPN (validation).
// Set this to 0 once you go live or don't require logging.
define("DEBUG", 1);
// Set to 0 once you're ready to go live
define("USE_SANDBOX", 1);
define("LOG_FILE", "./ipn.log");
// Read POST data
// reading posted data directly from $_POST causes serialization
// issues with array data in POST. Reading raw POST data from input stream instead.
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
$keyval = explode ('=', $keyval);
if (count($keyval) == 2)
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
if(function_exists('get_magic_quotes_gpc')) {
$get_magic_quotes_exists = true;
}
foreach ($myPost as $key => $value) {
if($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
$value = urlencode(stripslashes($value));
} else {
$value = urlencode($value);
}
$req .= "&$key=$value";
}
// Post IPN data back to PayPal to validate the IPN data is genuine
// Without this step anyone can fake IPN data
if(USE_SANDBOX == true) {
$paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr";
} else {
$paypal_url = "https://www.paypal.com/cgi-bin/webscr";
}
$ch = curl_init($paypal_url);
if ($ch == FALSE) {
return FALSE;
}
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
if(DEBUG == true) {
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLINFO_HEADER_OUT, 1);
}
// CONFIG: Optional proxy configuration
//curl_setopt($ch, CURLOPT_PROXY, $proxy);
//curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
// Set TCP timeout to 30 seconds
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
// CONFIG: Please download 'cacert.pem' from "http://curl.haxx.se/docs/caextract.html" and set the directory path
// of the certificate as shown below. Ensure the file is readable by the webserver.
// This is mandatory for some environments.
$cert = __DIR__ . "/lib/cacert.pem";
curl_setopt($ch, CURLOPT_CAINFO, $cert);
$res = curl_exec($ch);
if (curl_errno($ch) != 0) // cURL error
{
if(DEBUG == true) {
error_log(date('[Y-m-d H:i e] '). "Can't connect to PayPal to validate IPN message: " . curl_error($ch) . PHP_EOL, 3, LOG_FILE);
}
curl_close($ch);
exit;
} else {
// Log the entire HTTP response if debug is switched on.
if(DEBUG == true) {
error_log(date('[Y-m-d H:i e] '). "HTTP request of validation request:". curl_getinfo($ch, CURLINFO_HEADER_OUT) ." for IPN payload: $req" . PHP_EOL, 3, LOG_FILE);
error_log(date('[Y-m-d H:i e] '). "HTTP response of validation request: $res" . PHP_EOL, 3, LOG_FILE);
// Split response headers and payload
list($headers, $res) = explode("\r\n\r\n", $res, 2);
}
curl_close($ch);
}
// Inspect IPN validation result and act accordingly
if (strcmp ($res, "VERIFIED") == 0) {
//if (preg_match("!(VERIFIED)\s*\Z!",$res)) {
$payment_status = $_POST['payment_status'];
$payment_amount = $_POST['mc_gross'];
$payment_date = $_POST['payment_date'];
$txn_id = $_POST['txn_id'];
$receiver_business = $_POST['business'];
$payer_email = $_POST['payer_email'];
$email_hash = sha1($payer_email, TRUE);
$user_id = $_POST['custom'];
// Retrieve license information
$license = $license_amount[$payment_amount];
$nblic = $license_types[$license] - 1;
$name_license = $license_names[$license];
// Confirmation email variables
$subject_paypal = 'Premium License [' . $name_license . ']';
$body_paypal = '<p>Dear user.</p>';
if ( $payment_status == 'Completed' && $receiver_business == strtolower($paypal_business) ) {
/* DB interactions and checks */
$check = mysql_query("SELECT * FROM `premium` WHERE `id` = '$email_hash'");
/* 1) User already exists: Update DB */
if (mysql_num_rows($check) === 1) {
$row = mysql_fetch_assoc($check);
$transaction_id = $row['pa'];
if ($txn_id == $transaction_id) {
$donated = true;
die('Premium license already activated.');
} else {
$licenses_left = $row['la'];
$current_key = $row['k'];
$newnblic = $licenses_left + $nblic;
// UPDATE DB
mysql_query("UPDATE `premium` SET `lt` = '$license', `la` = '$newnblic', `t` = '$payment_date', `pa` = '$txn_id' WHERE `id` = '$email_hash' ");
// SEND EMAIL CONFIRMATION
GmSendMail($body_paypal, $subject_paypal, $payer_email);
}
/* 2) This is a NEW entry: Insert into DB
*/
} else {
// CREATE ENTRY INTO DB
mysql_query("INSERT INTO `premium` VALUES('$email_hash','$license','$nblic','$payment_date','$txn_id')");
// SEND EMAIL CONFIRMATION
GmSendMail($body_paypal, $subject_paypal, $payer_email);
}
}
if(DEBUG == true) {
error_log(date('[Y-m-d H:i e] '). "Verified IPN: $req ". PHP_EOL, 3, LOG_FILE);
}
} else if (strcmp ($res, "INVALID") == 0) {
// log for manual investigation
// Add business logic here which deals with invalid IPN messages
if(DEBUG == true) {
error_log(date('[Y-m-d H:i e] '). "Invalid IPN: $req" . PHP_EOL, 3, LOG_FILE);
}
}
The ipn.log file, when accessed directly from the browser reads:
[2014-02-15 11:53 America/Denver] HTTP request of validation request:POST /cgi-bin/webscr HTTP/1.1
Host: www.sandbox.paypal.com
Accept: */*
Connection: Close
Content-Length: 20
Content-Type: application/x-www-form-urlencoded
for IPN payload: cmd=_notify-validate
[2014-02-15 11:53 America/Denver] HTTP response of validation request: HTTP/1.1 200 OK
Date: Sat, 15 Feb 2014 18:53:08 GMT
Server: Apache
X-Frame-Options: SAMEORIGIN
Set-Cookie: < Cookie Hash >; domain=.paypal.com; path=/; Secure; HttpOnly
Set-Cookie: cookie_check=yes; expires=Tue, 13-Feb-2024 18:53:08 GMT; domain=.paypal.com; path=/; Secure; HttpOnly
Set-Cookie: navcmd=_notify-validate; domain=.paypal.com; path=/; Secure; HttpOnly
Set-Cookie: navlns=0.0; expires=Mon, 15-Feb-2016 18:53:08 GMT; domain=.paypal.com; path=/; Secure; HttpOnly
Set-Cookie: Apache=10.72.109.11.1392490388263396; path=/; expires=Mon, 08-Feb-44 18:53:08 GMT
Connection: close
Set-Cookie: X-PP-SILOVER=name%3DSANDBOX3.WEB.1%26silo_version%3D880%26app%3Dslingshot%26TIME%3D2495086418; domain=.paypal.com; path=/; Secure; HttpOnly
Set-Cookie: X-PP-SILOVER=; Expires=Thu, 01 Jan 1970 00:00:01 GMT
Set-Cookie: Apache=10.72.128.11.1392490388253446; path=/; expires=Mon, 08-Feb-44 18:53:08 GMT
Vary: Accept-Encoding
Strict-Transport-Security: max-age=14400
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8
INVALID
[2014-02-15 11:53 America/Denver] Invalid IPN: cmd=_notify-validate
For information, ipn.php is hosted on a subdomain with https enabled, .htaccess is clean and does not block any proxy. PHP 5.4 is used. I'm on a shared server with a dedicated IP.
I tried to uncomment the following lines in ipn.php:
// CONFIG: Optional proxy configuration
curl_setopt($ch, CURLOPT_PROXY, $proxy);
curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
with one time:
$proxy = 'mydedicatedIP:80';
another time:
$proxy = 'mydedicatedIP:443';
Without any success. Any help welcome !

How to retrieve particular header fields from a raw HTTP response?

This is what $result returns :
HTTP/1.1 200 OK
Server: SERVER
Content-Type: text/xml;charset=utf-8
Connection: close
Expires: Tue, 26 Mar 2013 00:28:45 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Date: Tue, 26 Mar 2013 00:28:45 GMT
Content-Length: 290
Connection: keep-alive
Set-Cookie: KEY=isbgvigbiwsb124252525252; Domain=www.website.com; Expires=Tue, 26-Mar-13 02:28:44 GMT; Path=/; HttpOnly
Set-Cookie: session=12345566789:abc1231552662626262; Domain=www.website.com; Expires=Thu, 25-Apr-2013 00:28:43 GMT; Path=/
<login>
<success>1</success>
<player>
<id>1234567</id>
<AnotherId>123456</AnotherId>
<email>email#email.com</email>
<accountinformation>
<id>123456</id>
<name>namehere</name>
<number>1234360</number>
</accountinformation>
</player>
</login>
I want to retrieve the KEY cookie from the response. Currently my code is as follows
//a cURL function would be here
$result = curl_exec($ch);
list($body, $split) = explode("\r\n\r\n", $result, 2);
$arr = explode("\r\n", $body);
$start = explode(":", $arr[10]);
$end = explode(";", $start[1]);
$INFO_I_NEED = $end[0];
What would be a simpler way of performing this action ? since it needs to be done 3/4 times for different parsing areas.
It looks like preg_match_all might be what you are looking for. Using this answer as inspiration try:
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $m);
You could then write a function like this:
function getCookies($result) {
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $m);
return($m)
}
$result = curl_exec($ch);
$cookiesArray = getCookies($result);
The return value of the function will be an array of all the cookie values. So $cookiesArray will hold:
array (
0 => 'KEY=isbgvigbiwsb124252525252',
1 => 'session=12345566789:abc1231552662626262',
)
Put it inside a function so you can reuse when need be:
<?php
//a cURL function would be here
$result = curl_exec($ch);
$INFO_I_NEED = myExplode($result);
function myExplode($data){
list($body, $split) = explode("\r\n\r\n", $result, 2);
$arr = explode("\r\n", $body);
$start = explode(":", $arr[10]);
$end = explode(";", $start[1]);
return($end[0]);
}
?>

PHP curl_exec returns both HTTP/1.1 100 Continue and HTTP/1.1 200 OK separated by space

I'm calling a service from PHP using cURL, like this:
$response = curl_exec($ch);
and the request/response headers look something like this:
Request:
POST /item/save HTTP/1.1
Host: services.mydomain.com
Accept: */*
Content-Length: 429
Expect: 100-continue
Content-Type: multipart/form-data
Response:
HTTP/1.1 100 Continue
HTTP/1.1 200 OK
Date: Fri, 06 Jul 2012 08:37:01 GMT
Server: Apache
Vary: Accept-Encoding,User-Agent
Content-Length: 256
Content-Type: application/json; charset=utf-8
followed by the body (json encoded data).
The problem is that the common thing is to split headers and body in the response by the first empty line encountered, except in this case, the empty line is after the 100 Continue and therefore everything else gets pushed into the body–and that is not valid json anymore :-)
So my question is this: What's the common way to deal with this?
I have 3 options lined up:
Specify that curl should not expect 100-continue? (How?)
Specify that curl should only send back the headers of the last response? (How?)
Manually check for 100 Continue headers and disregard them and their following empty line? (In that case, are there other similar things that could happen, that I should manually check for?)
Unless I'm missing something obvious, I'm sure people have stumbled upon this and solved it many times!
I will opt for #1.
You can force curl to send empty "Expect" header, by adding:
curl_setopt($ch, CURLOPT_HTTPHEADER,array("Expect:"));
to your code
If you want check it manually, you should define your own header callback and maybe write callback (look for CURLOPT_HEADERFUNCTION and CURLOPT_WRITEFUNCTION in curl_setopt doc), which has simply to ignore all "HTTP/1.1 100 Continue" headers.
Here's another method that uses the approach I described in the comment by parsing the response into header vs. body using CURLINFO_HEADER_SIZE:
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://test/curl_test.php");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLINFO_HEADER_OUT, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
// sets multipart/form-data content-type
curl_setopt($ch, CURLOPT_POSTFIELDS, array(
'field1' => 'foo',
'field2' => 'bar'
));
$data = curl_exec($ch);
// if you want the headers sent by CURL
$sentHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$header = substr($data, 0, $headerSize);
$body = substr($data, $headerSize);
echo "==Sent Headers==\n$sentHeaders\n==End Sent Headers==\n";
echo "==Response Headers==\n$headers\n==End Response Headers==\n";
echo "==Response Body==\n$body\n==End Body==";
I've tested this, and it results in the following output:
==Sent Headers==
POST /curl_test.php HTTP/1.1
Host: test
Accept: */*
Content-Length: 242
Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------
d86ac263ce1b
==End Sent Headers==
==Response Headers==
HTTP/1.1 100 Continue
HTTP/1.1 200 OK
Date: Fri, 06 Jul 2012 14:21:53 GMT
Server: Apache/2.4.2 (Win32) PHP/5.4.4
X-Powered-By: PHP/5.4.4
Content-Length: 112
Content-Type: text/plain
==End Response Headers==
==Response Body==
**FORM DATA**
array(2) {
["field1"]=>
string(3) "foo"
["field2"]=>
string(3) "bar"
}
**END FORM DATA**
==End Body==
i had the same problem but this solution does note work for me, finaly i have found this methode and all its fine:
we have to prepare data post fields before sending them:
function curl_custom_postfields($curl, array $assoc = array(), array $files = array()) {
/**
* For safe multipart POST request for PHP5.3 ~ PHP 5.4.
* #param resource $ch cURL resource
* #param array $assoc "name => value"
* #param array $files "name => path"
* #return bool
*/
// invalid characters for "name" and "filename"
static $disallow = array("\0", "\"", "\r", "\n");
// build normal parameters
foreach ($assoc as $key => $value) {
$key = str_replace($disallow, "_", $key);
$body[] = implode("\r\n", array(
"Content-Disposition: form-data; name=\"{$key}\"",
"",
filter_var($value),
));
}
// build file parameters
foreach ($files as $key => $value) {
switch (true) {
case false === $value = realpath(filter_var($value)):
case !is_file($value):
case !is_readable($value):
continue; // or return false, throw new InvalidArgumentException
}
$data = file_get_contents($value);
$value = call_user_func("end", explode(DIRECTORY_SEPARATOR, $value));
$key = str_replace($disallow, "_", $key);
$value = str_replace($disallow, "_", $value);
$body[] = implode("\r\n", array(
"Content-Disposition: form-data; name=\"{$key}\"; filename=\"{$value}\"",
"Content-Type: application/octet-stream",
"",
$data,
));
}
// generate safe boundary
do {
$boundary = "---------------------" . md5(mt_rand() . microtime());
} while (preg_grep("/{$boundary}/", $body));
// add boundary for each parameters
array_walk($body, function (&$part) use ($boundary) {
$part = "--{$boundary}\r\n{$part}";
});
// add final boundary
$body[] = "--{$boundary}--";
$body[] = "";
// set options
return #curl_setopt_array($curl, array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => implode("\r\n", $body),
CURLOPT_HTTPHEADER => array(
"Expect: 100-continue",
"Content-Type: multipart/form-data; boundary={$boundary}", // change Content-Type
),
));}
you have to prepare two arrays:
1- post field with normal data: (name1 = val1, name2 = val2, ...)
2- post field with file data: (name_file 1, path_file1, name_file2 = path_file2, ..)
and finaly call this function before executing curl like this.
$r = curl_custom_postfields($curl, $post, $postfields_files);
I have come across this with 100s and 302s etc it's annoying but sometimes needed (gdata calls etc) so i would say leave curl returning all headers and extract the body a little differently.
I handle it like this (can't find my actual code but you'll get the idea):
$response = curl_exec($ch);
$headers = array();
$body = array();
foreach(explode("\n\n", $response) as $frag){
if(preg_match('/^HTTP\/[0-9\.]+ [0-9]+/', $frag)){
$headers[] = $frag;
}else{
$body[] = $frag;
}
}
echo implode("\n\n", $headers);
echo implode("\n\n", $body);
I begrudge the longwinded hackish method (would prefer it if curl marked the body content somehow) but it has worked well over the years. let us know how you get on.

Returning header as array using Curl

I'm trying to get the response & the response headers from CURL using PHP, specifically for Content-Disposition: attachment; so I can return the filename passed within the header. This doesn't seem to get returned within curl_getinfo.
I've tried using the HeaderFunction to call a function to read the additional headers, however, I am unable to add the contents to an array.
Does anyone have any ideas please?
Below is part of my code which is a Curl wrapper class:
...
curl_setopt($this->_ch, CURLOPT_URL, $this->_url);
curl_setopt($this->_ch, CURLOPT_HEADER, false);
curl_setopt($this->_ch, CURLOPT_POST, 1);
curl_setopt($this->_ch, CURLOPT_POSTFIELDS, $this->_postData);
curl_setopt($this->_ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->_ch, CURLOPT_USERAGENT, $this->_userAgent);
curl_setopt($this->_ch, CURLOPT_HEADERFUNCTION, 'readHeader');
$this->_response = curl_exec($this->_ch);
$info = curl_getinfo($this->_ch);
...
function readHeader($ch, $header)
{
array_push($this->_headers, $header);
}
Here, this should do it:
curl_setopt($this->_ch, CURLOPT_URL, $this->_url);
curl_setopt($this->_ch, CURLOPT_HEADER, 1);
curl_setopt($this->_ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($this->_ch);
$info = curl_getinfo($this->_ch);
$headers = get_headers_from_curl_response($response);
function get_headers_from_curl_response($response)
{
$headers = array();
$header_text = substr($response, 0, strpos($response, "\r\n\r\n"));
foreach (explode("\r\n", $header_text) as $i => $line)
if ($i === 0)
$headers['http_code'] = $line;
else
{
list ($key, $value) = explode(': ', $line);
$headers[$key] = $value;
}
return $headers;
}
The anwser from c.hill is great but the code will not handle if the first response is a 301 or 302 - in that case only the first header will be added to the array returned by get_header_from_curl_response().
I've updated the function to return an array with each of the headers.
First I use this lines to create a variable with only the header content
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($a, 0, $header_size);
Than I pass $header in to the new get_headers_from_curl_response()-function:
static function get_headers_from_curl_response($headerContent)
{
$headers = array();
// Split the string on every "double" new line.
$arrRequests = explode("\r\n\r\n", $headerContent);
// Loop of response headers. The "count() -1" is to
//avoid an empty row for the extra line break before the body of the response.
for ($index = 0; $index < count($arrRequests) -1; $index++) {
foreach (explode("\r\n", $arrRequests[$index]) as $i => $line)
{
if ($i === 0)
$headers[$index]['http_code'] = $line;
else
{
list ($key, $value) = explode(': ', $line);
$headers[$index][$key] = $value;
}
}
}
return $headers;
}
This function will take header like this:
HTTP/1.1 302 Found
Cache-Control: no-cache
Pragma: no-cache
Content-Type: text/html; charset=utf-8
Expires: -1
Location: http://www.website.com/
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
Date: Sun, 08 Sep 2013 10:51:39 GMT
Connection: close
Content-Length: 16313
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
Date: Sun, 08 Sep 2013 10:51:39 GMT
Connection: close
Content-Length: 15519
And return an array like this:
(
[0] => Array
(
[http_code] => HTTP/1.1 302 Found
[Cache-Control] => no-cache
[Pragma] => no-cache
[Content-Type] => text/html; charset=utf-8
[Expires] => -1
[Location] => http://www.website.com/
[Server] => Microsoft-IIS/7.5
[X-AspNet-Version] => 4.0.30319
[Date] => Sun, 08 Sep 2013 10:51:39 GMT
[Connection] => close
[Content-Length] => 16313
)
[1] => Array
(
[http_code] => HTTP/1.1 200 OK
[Cache-Control] => private
[Content-Type] => text/html; charset=utf-8
[Server] => Microsoft-IIS/7.5
[X-AspNet-Version] => 4.0.30319
[Date] => Sun, 08 Sep 2013 10:51:39 GMT
[Connection] => close
[Content-Length] => 15519
)
)
Using the array() form for method callbacks should make the original example work:
curl_setopt($this->_ch, CURLOPT_HEADERFUNCTION, array($this, 'readHeader'));
Another my implementation:
function getHeaders($response){
if (!preg_match_all('/([A-Za-z\-]{1,})\:(.*)\\r/', $response, $matches)
|| !isset($matches[1], $matches[2])){
return false;
}
$headers = [];
foreach ($matches[1] as $index => $key){
$headers[$key] = $matches[2][$index];
}
return $headers;
}
Used in case, which request format is:
Host: *
Accept: *
Content-Length: *
and etc ...
Simple and straightforward
$headers = [];
// Get the response body as string
$response = curl_exec($curl);
// Get the response headers as string
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
// Get the substring of the headers and explode as an array by \r\n
// Each element of the array will be a string `Header-Key: Header-Value`
// Retrieve this two parts with a simple regex `/(.*?): (.*)/`
foreach(explode("\r\n", trim(substr($response, 0, $headerSize))) as $row) {
if(preg_match('/(.*?): (.*)/', $row, $matches)) {
$headers[$matches[1]] = $matches[2];
}
}
Fixing issues:
Error when content of the header contained ': '(split string)
Multiline-headers were not supported
Duplicate headers (Set-Cookie) were not supported
This is my take on the topic ;-)
list($head, $body)=explode("\r\n\r\n", $content, 2);
$headers=parseHeaders($head);
function parseHeaders($text) {
$headers=array();
foreach (explode("\r\n", $text) as $i => $line) {
// Special HTTP first line
if (!$i && preg_match('#^HTTP/(?<protocol>[0-9.]+)\s+(?<code>\d+)(?:\s+(?<message>.*))?$#', $line, $match)) {
$headers['#status']=$line;
$headers['#code']=$match['code'];
$headers['#protocol']=$match['protocol'];
$headers['#message']=$match['message'];
continue;
}
// Multiline header - join with previous
if ($key && preg_match('/^\s/', $line)) {
$headers[$key].=' '.trim($line);
continue;
}
list ($key, $value) = explode(': ', $line, 2);
$key=strtolower($key);
// Append duplicate headers - namely Set-Cookie header
$headers[$key]=isset($headers[$key]) ? $headers[$key].' ' : $value;
}
return $headers;
}
C.hill's answer is great but breaks when retrieving multiple cookies. Made the change here
public function get_headers_from_curl_response($response) {
$headers = array();
$header_text = substr($response, 0, strpos($response, "\r\n\r\n"));
foreach (explode("\r\n", $header_text) as $i => $line)
if ($i === 0) $headers['http_code'] = $line;
else {
list ($key, $value) = explode(': ', $line); $headers[$key][] = $value;
}
return $headers;
}
You can use http_parse_headers function.
It comes from PECL but you will find fallbacks in this SO thread.
you can do 2 ways
by set curl_setopt($this->_ch, CURLOPT_HEADER, true);
header will come out with response message from curl_exec();
you must search keyword 'Content-Disposition:' from response message.
by use this function get_headers($url) right after you call curl_exec(). $url is url called in curl. the return is array of headers. search for 'Content-Disposition' in array to get what you want.

Categories