Paypal IPN Verification Postback with HTTPS - php

According to new security requirements (2016, 2017 and 2018), it seems that HTTPS will be required for exchange between server and Paypal, during an "IPN". This question is linked to this subject and also this.
How should we adapt this PHP IPN code?
$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
$header .= "Host: www.paypal.com:80\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);
$req = 'cmd=_notify-validate';
...
fputs ($fp, $header . $req);
Would replacing the two occurences of www.paypal.com by https://www.paypal.com be enough?
Also, is the fact my shop website is not HTTPS a problem, will this connection be refused?
Here is part of the email received from Paypal:
Edit (2018/06/22), here is the actual IPN code, after applying the accepted answer code. Strangely, I still get: "IPN Verification postback to HTTPS. Update needed: YES". So this means the following code is still not 100% compliant to HTTPS. Why?
<?php
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value) {
$value = trim(urlencode(stripslashes($value)));
$req .= "&$key=$value";
}
$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
$header .= "Host: www.paypal.com\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
$fp = fsockopen('tls://www.paypal.com', 443, $errno, $errstr, 30);
// variables
$item_name = $_POST['item_name'];
$business = $_POST['business'];
$item_number = $_POST['item_number'];
$payment_status = $_POST['payment_status'];
// and many more
if (!$fp)
{
// HTTP ERROR
} else
{
fputs ($fp, $header . $req);
while (!feof($fp))
{
$res = fgets ($fp, 1024);
if (strcmp ($res, "VERIFIED") == 0)
{
// send email to customer, etc.
}
}
fclose ($fp);
}
?>

hostname
If OpenSSL support is installed, you may prefix the hostname with either ssl:// or tls:// to use an SSL or TLS client connection over TCP/IP to connect to the remote host.
http://www.php.net/fsockopen
The port would also need to change to 443. So:
$header .= "Host: www.paypal.com\r\n";
...
$fp = fsockopen('ssl://www.paypal.com', 443, $errno, $errstr, 30);
...
fputs ($fp, $header . $req);
https:// would not work because you're opening a socket, which is a low-level transport. HTTP is an application level protocol on top of that, which the socket doesn't know or care about. At the socket level it's a TLS connection.
Also, is the fact my shop website is not HTTPS a problem, will this connection be refused?
What kind of connection a browser has to your server is irrelevant and nobody knows that. You're opening a socket from a PHP program to Paypal, you may as well be doing that directly from the command line without any "HTTP" connection involved at all.

I would suggest you abandon fsocket in favour of curl. Its a lot more reliable (I think). Ive just updated my curl component within my ipn.php to be compliant with Paypals requirements of HTTP1/1 and TLS1.2 (sslversion=6)
The code is
//NOW SEND IT ALL BACK TO PAYPAL AS CONFIRMATION//
// USING CURL AS PHP FSOCKOPEN IS LESS RELIABLE //
$curl_result=$curl_err='';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$paypal_url);
curl_setopt($ch, CURLOPT_CAINFO, "cacert.pem");
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: application/x-www-form-urlencoded", "Content-Length: " . strlen($req)));
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$curl_result = #curl_exec($ch);
$curl_err = curl_error($ch);
curl_close($ch);
It works fine

Further to my comments about Curl, Paypal developers have supplied several sets of source code for IPN. They supply C++, Python, ASP, PHP, and a whole bunch more. You will find them on github. All of the PHP solutions use Curl. Even though some of the uploads are a few years old they must have known that Paypal would update their full comms (for IPN) to HTTPS.
Even with some of the older packages, a simple adding of 1 line of code to set SSL version to 6 and the old codes are good to go for Paypals new requirements

The link at https://github.com/paypal/ipn-code-samples is a compilation of codes in Python, Ruby, Perl, Asp and a few others including PHP.
A direct link to the PHP component is at https://github.com/paypal/ipn-code-samples/tree/master/php
This is the one I use because its already HTTP1/1 and TLS1.2 "ready" and therefore ticks all the boxes for Paypal's new security compliance.
There area few other github samples, some over 6 years old, but this is the most up to date

You should probably also change that HTTP/1.0 to HTTP/1.1 now, as per PayPal's latest requirements. So that's:
$header .= "POST /cgi-bin/webscr HTTP/1.1\r\n";

In addition to deceze's answer, PayPal's update now requires you to use ipnpb.paypal.com
Try using this code instead.
$fp = fsockopen('ssl://ipnpb.paypal.com', "443", $err_num, $err_str, 60);

Related

Opening connection to HTTP server with PHP

I'm trying to open a connection to a webserver I have hosted on an Amazon EC2 instance. I can access the port with a GET request from my browser which gives me the following:
Started HTTP server...
- <IP redacted> [19/Jul/2013 13:49:24] code 501, message Unsupported method ('GET')
- <IP redacted> [19/Jul/2013 13:49:24] "GET / HTTP/1.1" 501 -
This is as expected as I haven't implemented GET on the webserver - but I get no response at all using the following snippet to POST to the same port.
<?php
$req = 'verify=true';
$header = "POST / HTTP/1.1\r\n";
$header .= "Host: ec2-55-555-555-555.compute-1.amazonaws.com\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
$fp = fsockopen ("ec2-55-555-555-555.compute-1.amazonaws.com", 8080, $errno, $errstr, 30);
fputs ($fp, $header . $req);
fclose ($fp);
?>
It definitely runs and reaches the end but the fsockopen call is timing out. What's missing or is there but shouldn't be?
The easiest way to interact with an HTTP server from PHP would be to use curl.
It's a good library with some great features. It works well.
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "Your_url_here");
$resp = curl_exec($curl);

I get missings POST params in my php IPN callback after iPhone SDK payment

I am using Paypal SDK 1.6 for iOS.
I have a IPN callback php script and When I test from IPN Paypal Simulator it works fine (Payment verified) And when I test from an iPhone payment I have some missing POST parameters.
Edit : Code
<?php
// Revision Notes
// 11/04/11 - changed post back url from https://www.paypal.com/cgi-bin/webscr to https://ipnpb.paypal.com/cgi-bin/webscr
// For more info see below:
// https://www.x.com/content/bulletin-ip-address-expansion-paypal-services
//"ACTION REQUIRED: if you are using IPN (Instant Payment Notification) for Order Management and your IPN listener script is behind a firewall that uses ACL (Access Control List) rules which restrict outbound traffic to a limited number of IP addresses, then you may need to do one of the following:
// To continue posting back to https://www.paypal.com to perform IPN validation you will need to update your firewall ACL to allow outbound access to *any* IP address for the servers that host your IPN script
// OR Alternatively, you will need to modify your IPN script to post back IPNs to the newly created URL https://ipnpb.paypal.com using HTTPS (port 443) and update firewall ACL rules to allow outbound access to the ipnpb.paypal.com IP ranges (see end of message)."
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value) {
$value = urlencode(stripslashes($value));
$req .= "&$key=$value";
}
// post back to PayPal system to validate
$header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
// If testing on Sandbox use:
$header .= "Host: www.sandbox.paypal.com:443\r\n";
//$header .= "Host: ipnpb.paypal.com:443\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
// If testing on Sandbox use:
$fp = fsockopen ('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30);
//$fp = fsockopen ('ssl://ipnpb.paypal.com', 443, $errno, $errstr, 30);
// assign posted variables to local variables
$item_name = $_POST['item_name'];
$item_number = $_POST['item_number'];
$payment_status = $_POST['payment_status'];
$payment_amount = $_POST['mc_gross'];
$payment_currency = $_POST['mc_currency'];
$txn_id = $_POST['txn_id'];
$receiver_email = $_POST['receiver_email'];
$payer_email = $_POST['payer_email'];
if (!$fp) {
// HTTP ERROR
} else {
fputs ($fp, $header . $req);
while (!feof($fp)) {
$res = fgets ($fp, 1024);
if (strcmp ($res, "VERIFIED") == 0) {
// check the payment_status is Completed
// check that txn_id has not been previously processed
// check that receiver_email is your Primary PayPal email
// check that payment_amount/payment_currency are correct
// process payment
$mail_From = "From: me#mybiz.com";
$mail_To = "testenter code here#gmail.com";
$mail_Subject = "VERIFIED IPN";
$mail_Body = $req;
mail($mail_To, $mail_Subject, $mail_Body, $mail_From);
}
else if (strcmp ($res, "INVALID") == 0) {
// log for manual investigation
$mail_From = "From: me#mybiz.com";
$mail_To = "test#gmail.com";
$mail_Subject = "INVALID IPN";
$mail_Body = $req;
mail($mail_To, $mail_Subject, $mail_Body, $mail_From);
}
}
fclose ($fp);
}
?>
The IPN from the sandbox simulates an Express Checkout transaction, which is a different type of transaction than what you get from using the Mobile Payments Library.
One item that you are probably getting is the PayKey. With the PayKey you can make a second API call to the PaymentDetails API, which should yield the rest of the information you're looking for.
Ref: Payment Details API
I change the request method with curl and now my script work fine:
$ch = curl_init('https://www.paypal.com/cgi-bin/webscr');
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);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
// In wamp like environments that do not come bundled with root authority certificates,
// please download 'cacert.pem' from "http://curl.haxx.se/docs/caextract.html" and set the directory path
// of the certificate as shown below.
curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/cacert.pem');
if( !($res = curl_exec($ch)) ) {
// error_log("Got " . curl_error($ch) . " when processing IPN data");
curl_close($ch);
exit;
}
curl_close($ch);

PUT file (content) with PHP and cURL

I'm trying to upload a file using cURL and the PUT method, I have already a function that works using fsockopen but i would like to migrate it to cURL.
The function that uses fsockopen receives the content of a file, the filename and the credentials for auth and make the request:
function put_file($content, $filename, $username, $pass)
{
$header = "PUT /upload?username=".urlencode(user_name)."&passwd=".urlencode($pass)."&filename=".urlencode($file_name)." HTTP/1.0\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($content) . "\r\n\r\n";
$fp = #fsockopen("ssl://URL", 443, $errno, $errstr, 30);
if(!$fp)
{
return "ERROR";
}
else
{
fputs ($fp, $header.$content);
while (!feof($fp))
{
$res .= fread ($fp, 1024);
}
fclose($fp);
}
}
I have been trying to migrate that function to cURL, but I don't know how to do it without the need of have a "real" file on my filesystem. The only cURL options I know for this are CURLOPT_INFILE and CURLOPT_INFILESIZE, but I don't have the file (and don't want to write it to disk and after open it).
What I need is to send the "content" of the file, just like the fsockopen version does. How can this be achieved with cURL?
Thank you in advanced.
You could use php://temp wrapper, which is a temporary file stream in PHP.
First you write the data to the stream (don't forget to use rewind() so cURL will read all data):
$fp = fopen("php://temp", "r+");
fputs($fp, $content);
rewind($fp);
Then when setting up the cURL just use:
curl_setopt($ch, CURLOPT_INFILE, $fp);
curl_setopt($ch, CURLOPT_INFILESIZE, strlen($content)); #adding missing bracket
And at the end close temp file handler (optional):
fclose($fp);

Paypal PDT error 4003

After many hours of messing about and trying to set up what should be the relatively simple process of sending a payment to www.sandbox.paypal.com and being redirected back to a page on my site with a transaction id in the querystring, I have finally achieved it.
I am now receiving an error message 'FAIL Error 4003'.
Here is the code I am using. It is pretty much the same as the paypal example (all I have done is echo out the responses):
<?php
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-synch';
$tx_token = $_GET['tx'];
$auth_token = "ZdoN6q4GLiRniR2BbOzEEF22GJOWHpVOXRtP7fAhBpvwwm5GyWcTzO_sSSO";
$req .= "&tx=$tx_token&at=$auth_token";
// post back to PayPal system to validate
$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);
// If possible, securely post back to paypal using HTTPS
// Your PHP server will need to be SSL enabled
// $fp = fsockopen ('ssl://www.paypal.com', 443, $errno, $errstr, 30);
if (!$fp)
{
// HTTP ERROR
echo "HTTP Error";
}
else
{
fputs ($fp, $header . $req);
// read the body data
$res = '';
$headerdone = false;
while (!feof($fp))
{
$line = fgets ($fp, 1024);
if (strcmp($line, "\r\n") == 0) {
// read the header
$headerdone = true;
}
else if ($headerdone)
{
// header has been read. now read the contents
$res .= $line;
echo $line;
}
}
// parse the data
$lines = explode("\n", $res);
$keyarray = array();
if (strcmp ($lines[0], "SUCCESS") == 0)
{
for ($i=1; $i<count($lines);$i++)
{
list($key,$val) = explode("=", $lines[$i]);
$keyarray[urldecode($key)] = urldecode($val);
}
// check the payment_status is Completed
// check that txn_id has not been previously processed
// check that receiver_email is your Primary PayPal email
// check that payment_amount/payment_currency are correct
// process payment
$firstname = $keyarray['first_name'];
$lastname = $keyarray['last_name'];
$itemname = $keyarray['item_name'];
$amount = $keyarray['payment_gross'];
echo ("<p><h3>Thank you for your purchase!</h3></p>");
echo ("<b>Payment Details</b><br>\n");
echo ("<li>Name: $firstname $lastname</li>\n");
echo ("<li>Item: $itemname</li>\n");
echo ("<li>Amount: $amount</li>\n");
echo ("");
}
else if (strcmp ($lines[0], "FAIL") == 0) {
echo "Failure: " . $lines[0];
// log for manual investigation
}
}
fclose ($fp);
?>
<br />
Thank you for your payment. Your transaction has been completed, and a receipt for your purchase has been emailed to you. You may log into your account at www.sandbox.paypal.com/ie to view details of this transaction.
I have made sure to confirm the email addresses for both my sandbox merchant and buyer accounts and enabled PDT.
The client is redirected correctly back to my 'thank you' page with the following querystring paramaters - ?tx=4FU63684496248523&st=Pending&amt=29.90&cc=EUR&cm=&item_number=
Has anyone else encountered this error message? If so, what are the usual causes?
The problem was that I was sending my test http request to paypal.com rather than sandbox.paypal.com. The answer was in the FAQ left by Jukebox.
Check the script.When testing Payment Data Transfer (PDT) in the
Sandbox, make sure your PDT script POSTs back information to
www.sandbox.paypal.com. If testing on the Live PayPal site, make sure
the script POSTs data back to www.paypal.com. Currently, all sample
code on the Live and Sandbox sites "point" back to the live PayPal
site.
Hope it helps someone else get up and running quicker than I did. I am now onto my next issue which is the token being returned as empty rather than an error.. sigh..
Make sure your transaction ID is not expired. 4003 PDT error code also comes when your transaction ID is expired.
Here's what solved it for me...
In the Buy Now button, I had a business email address associated with my LIVE PayPal account whereas I need to use the business email address associated with my SANDBOX PayPal account as in:
<input type="hidden" name="business" value="user#host.com">
The value attribute needed to reflect the business email address associated with my sandbox account.
By the way, here's an alternate PHP cURL version of the above script that also does the trick of eliciting a response from PayPal's sandbox:
if (isset($_GET['tx'])) {
$tx = $_GET['tx'];
$identity_token = "INSERT_YOUR_IDENTITY_TOKEN_HERE";
//echo $tx;
$url = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
$nvpString="cmd=_notify-synch".
"&tx=$tx".
"&at=$identity_token";
//echo $nvpString;
//define where the data is going to
$curl = curl_init($url);
//tell cURL to fail if an error occurs
curl_setopt($curl, CURLOPT_FAILONERROR, 1);
//allow for redirects
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
//assign the returned data to a variable
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
//set the timeout
curl_setopt($curl, CURLOPT_TIMEOUT, 60);
//use POST
curl_setopt($curl, CURLOPT_POST, 1);
//set the POST data
curl_setopt($curl, CURLOPT_POSTFIELDS, $nvpString);
//execute the transaction
$response = curl_exec($curl);
//show errors
curl_error($curl);
//close the connection
curl_close($curl);
echo '<pre>';
print_r($response);
echo '</pre>';
}//end if (isset($_GET['tx']))
The Identity Token of my test seller account changed without notification. Using the new (correct) Identity Token fixed the problem.
Some more information on Paypal’s PTD can be found here:
http://www.secure-ebook.com/help/payment:paypal:fail_4003
$tx=$_REQUEST['tx'];
$paypal_url='https://www.paypal.com/cgi-bin/webscr?cmd=_notify-synch&tx='.$tx.'&at=token here';
$curl = curl_init($paypal_url);
$data = array(
"cmd" => "_notify-synch",
"tx" => $tx,
"at" => "token here"
);
$data_string = json_encode($data);
curl_setopt ($curl, CURLOPT_HEADER, 0);
curl_setopt ($curl, CURLOPT_POST, 1);
curl_setopt ($curl, CURLOPT_POSTFIELDS, $data_string);
curl_setopt ($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt ($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($curl, CURLOPT_SSL_VERIFYHOST, 1);
$headers = array (
'Content-Type: application/x-www-form-urlencoded',
'Host: www.paypal.com',
'Connection: close'
);
curl_setopt ($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt ($curl, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($curl);
$lines = explode("\n", $response);
$keyarray = array();
if (strcmp ($lines[0], "SUCCESS") == 0) {
for ($i=1; $i<count($lines);$i++){
list($key,$val) = explode("=", $lines[$i]);
$keyarray[urldecode($key)] = urldecode($val);
}
$first_name=$keyarray['first_name'];
$last_name=$keyarray['last_name'];
$payment_status=$keyarray['payment_status'];
$business=$keyarray['business'];
$payer_email=$keyarray['payer_email'];
$payment_gross=$keyarray['payment_gross'];
$mc_currency=$keyarray['mc_currency'];
}

Show Curl POST Request Headers? Is there a way to do this?

I'm building a Curl web automation app and am having some issue with not getting the desired outcome of my POST action, I am having some trouble figuring out how I can show the full POST request I am sending over (with headers), I have been searching on this but everything that comes up is the response headers, actually I want these too but also the request, which none of the posts I find on google seem to mention..
I know I can display the result of a curl request using something like this (forgive me if my syntax is off, I already shut down my virtual machine with my ide and code to refer to
$result = curl($curl_exect) ;
Anyways, I would greatly appreciate any advice on how to view the full headers, thanks
Here is all you need:
curl_setopt($curlHandle, CURLINFO_HEADER_OUT, true); // enable tracking
... // do curl request
$headerSent = curl_getinfo($curlHandle, CURLINFO_HEADER_OUT ); // request headers
You can see the information regarding the transfer by doing:
curl_setopt($curl_exect, CURLINFO_HEADER_OUT, true);
before the request, and
$information = curl_getinfo($curl_exect);
after the request
View: http://www.php.net/manual/en/function.curl-getinfo.php
You can also use the CURLOPT_HEADER in your curl_setopt
curl_setopt($curl_exect, CURLOPT_HEADER, true);
$httpcode = curl_getinfo($c, CURLINFO_HTTP_CODE);
return $httpcode == 200;
These are just some methods of using the headers.
You can save all headers sent by curl to a file using :
$f = fopen('request.txt', 'w');
curl_setopt($ch,CURLOPT_VERBOSE,true);
curl_setopt($ch,CURLOPT_STDERR ,$f);
You can make you request headers by yourself using:
// open a socket connection on port 80
$fp = fsockopen($host, 80);
// send the request headers:
fputs($fp, "POST $path HTTP/1.1\r\n");
fputs($fp, "Host: $host\r\n");
fputs($fp, "Referer: $referer\r\n");
fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");
fputs($fp, "Content-length: ". strlen($data) ."\r\n");
fputs($fp, "Connection: close\r\n\r\n");
fputs($fp, $data);
$result = '';
while(!feof($fp)) {
// receive the results of the request
$result .= fgets($fp, 128);
}
// close the socket connection:
fclose($fp);
Like writen on how make request
I had exactly the same problem lately, and I installed Wireshark (it is a network monitoring tool). You can see everything with this, except encrypted traffic (HTTPS).

Categories