With no changes on our side and perhaps related to POODLE/SSL3 our PayPal API call to PPIPNMessage::validate is now failing with.
SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure
The checkout and receipt of IPN is fine (and we have never supported SSL3 incoming), it's just failing when acknowledging the IPN (oddly PayPal doesn't try again, even though we have failed)
Running curl from the same server command line succeeds
$ curl -iv https://ipnpb.paypal.com/cgi-bin/webscr * About to connect() to ipnpb.paypal.com port 443 (#0) * Trying 173.0.88.8... connected * successfully set certificate verify locations: * CAfile: none CApath: /etc/ssl/certs * SSLv3, TLS handshake, Client hello (1): * SSLv3, TLS handshake, Server hello (2): * SSLv3, TLS handshake, CERT (11): * SSLv3, TLS handshake, Server finished (14): * SSLv3, TLS handshake, Client key exchange (16): * SSLv3, TLS change cipher, Client hello (1): * SSLv3, TLS handshake, Finished (20): * SSLv3, TLS change cipher, Client hello (1): * SSLv3, TLS handshake, Finished (20): * SSL connection using AES256-SHA * Server certificate: * subject: 1.3.6.1.4.1.311.60.2.1.3=US; 1.3.6.1.4.1.311.60.2.1.2=Delaware; businessCategory=Private Organization; serialNumber=3014267; C=US; postalCode=95131-2021; ST=California; L=San Jose; street=2211 N 1st St; O=PayPal, Inc.; OU=PayPal Production; CN=ipnpb.paypa * start date: 2013-06-28 00:00:00 GMT * expire date: 2015-08-02 23:59:59 GMT * subjectAltName: ipnpb.paypal.com matched * issuer: C=US; O=VeriSign, Inc.; OU=VeriSign Trust Network; OU=Terms of use at https://www.verisign.com/rpa (c)06; CN=VeriSign Class 3 Extended Validation SSL CA * SSL certificate verify ok. > GET /cgi-bin/webscr HTTP/1.1 > User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3 > Host: ipnpb.paypal.com > Accept: */*
I did note that ssllabs.com shows 1 out of 4 IPs still supporting SSL3 on this endpoint.
This is the same problem as Error 0x1408F10B: "SSL3_GET_RECORD:wrong version number" with PayPal SDK
The version of PayPal API we are using hard codes CURLOPT_SSLVERSION to 3.
Our fix is to insert this before any PayPal calls.
PPHttpConfig::$DEFAULT_CURL_OPTS[CURLOPT_SSLVERSION] = 4;
I had the same problem... Just change the following line on the ipnlistener.php
from:
curl_setopt($ch, CURLOPT_SSLVERSION, 3);
to:
curl_setopt($ch, CURLOPT_SSLVERSION, 4);
ipnlistener.php
<?php /** * PayPal IPN Listener * * A class to listen for and handle Instant Payment Notifications (IPN) from * the PayPal server. * * https://github.com/Quixotix/PHP-PayPal-IPN * * @package PHP-PayPal-IPN * @author Micah Carrick * @copyright (c) 2012 - Micah Carrick * @version 2.1.0 */ class IpnListener { /** * If true, the recommended cURL PHP library is used to send the post back * to PayPal. If flase then fsockopen() is used. Default true. * * @var boolean */ public $use_curl = true; /** * If true, explicitly sets cURL to use SSL version 3. Use this if cURL * is compiled with GnuTLS SSL. * * @var boolean */ public $force_ssl_v3 = true; /** * If true, cURL will use the CURLOPT_FOLLOWLOCATION to follow any * "Location: ..." headers in the response. * * @var boolean */ public $follow_location = false; /** * If true, an SSL secure connection (port 443) is used for the post back * as recommended by PayPal. If false, a standard HTTP (port 80) connection * is used. Default true. * * @var boolean */ public $use_ssl = true; /** * If true, the paypal sandbox URI www.sandbox.paypal.com is used for the * post back. If false, the live URI www.paypal.com is used. Default false. * * @var boolean */ public $use_sandbox = false; /** * The amount of time, in seconds, to wait for the PayPal server to respond * before timing out. Default 30 seconds. * * @var int */ public $timeout = 30; private $post_data = array(); private $post_uri = ''; private $response_status = ''; private $response = ''; const PAYPAL_HOST = 'www.paypal.com'; const SANDBOX_HOST = 'www.sandbox.paypal.com'; /** * Post Back Using cURL * * Sends the post back to PayPal using the cURL library. Called by * the processIpn() method if the use_curl property is true. Throws an * exception if the post fails. Populates the response, response_status, * and post_uri properties on success. * * @param string The post data as a URL encoded string */ protected function curlPost($encoded_data) { if ($this->use_ssl) { $uri = 'https://'.$this->getPaypalHost().'/cgi-bin/webscr'; $this->post_uri = $uri; } else { $uri = 'http://'.$this->getPaypalHost().'/cgi-bin/webscr'; $this->post_uri = $uri; } $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__)."/cert/api_cert_chain.crt"); curl_setopt($ch, CURLOPT_URL, $uri); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded_data); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $this->follow_location); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_SSLVERSION, 4); if ($this->force_ssl_v3) { curl_setopt($ch, CURLOPT_SSLVERSION, 4); //Modified from 3 to 4 } $this->response = curl_exec($ch); $this->response_status = strval(curl_getinfo($ch, CURLINFO_HTTP_CODE)); if ($this->response === false || $this->response_status == '0') { $errno = curl_errno($ch); $errstr = curl_error($ch); throw new Exception("cURL error: [$errno] $errstr"); } } /** * Post Back Using fsockopen() * * Sends the post back to PayPal using the fsockopen() function. Called by * the processIpn() method if the use_curl property is false. Throws an * exception if the post fails. Populates the response, response_status, * and post_uri properties on success. * * @param string The post data as a URL encoded string */ protected function fsockPost($encoded_data) { if ($this->use_ssl) { $uri = 'ssl://'.$this->getPaypalHost(); $port = '443'; $this->post_uri = $uri.'/cgi-bin/webscr'; } else { $uri = $this->getPaypalHost(); // no "http://" in call to fsockopen() $port = '80'; $this->post_uri = 'http://'.$uri.'/cgi-bin/webscr'; } $fp = fsockopen($uri, $port, $errno, $errstr, $this->timeout); if (!$fp) { // fsockopen error throw new Exception("fsockopen error: [$errno] $errstr"); } $header = "POST /cgi-bin/webscr HTTP/1.1\r\n"; $header .= "Host: ".$this->getPaypalHost()."\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: ".strlen($encoded_data)."\r\n"; $header .= "Connection: Close\r\n\r\n"; fputs($fp, $header.$encoded_data."\r\n\r\n"); while(!feof($fp)) { if (empty($this->response)) { // extract HTTP status from first line $this->response .= $status = fgets($fp, 1024); $this->response_status = trim(substr($status, 9, 4)); } else { $this->response .= fgets($fp, 1024); } } fclose($fp); } private function getPaypalHost() { if ($this->use_sandbox) return self::SANDBOX_HOST; else return self::PAYPAL_HOST; } /** * Get POST URI * * Returns the URI that was used to send the post back to PayPal. This can * be useful for troubleshooting connection problems. The default URI * would be "ssl://www.sandbox.paypal.com:443/cgi-bin/webscr" * * @return string */ public function getPostUri() { return $this->post_uri; } /** * Get Response * * Returns the entire response from PayPal as a string including all the * HTTP headers. * * @return string */ public function getResponse() { return $this->response; } /** * Get Response Status * * Returns the HTTP response status code from PayPal. This should be "200" * if the post back was successful. * * @return string */ public function getResponseStatus() { return $this->response_status; } /** * Get Text Report * * Returns a report of the IPN transaction in plain text format. This is * useful in emails to order processors and system administrators. Override * this method in your own class to customize the report. * * @return string */ public function getTextReport() { $r = ''; // date and POST url for ($i=0; $i<80; $i++) { $r .= '-'; } $r .= "\n[".date('m/d/Y g:i A').'] - '.$this->getPostUri(); if ($this->use_curl) $r .= " (curl)\n"; else $r .= " (fsockopen)\n"; // HTTP Response for ($i=0; $i<80; $i++) { $r .= '-'; } $r .= "\n{$this->getResponse()}\n"; // POST vars for ($i=0; $i<80; $i++) { $r .= '-'; } $r .= "\n"; foreach ($this->post_data as $key => $value) { $r .= str_pad($key, 25)."$value\n"; } $r .= "\n\n"; return $r; } /** * Process IPN * * Handles the IPN post back to PayPal and parsing the response. Call this * method from your IPN listener script. Returns true if the response came * back as "VERIFIED", false if the response came back "INVALID", and * throws an exception if there is an error. * * @param array * * @return boolean */ public function processIpn($post_data=null) { $encoded_data = 'cmd=_notify-validate'; if ($post_data === null) { // use raw POST data if (!empty($_POST)) { $this->post_data = $_POST; $encoded_data .= '&'.file_get_contents('php://input'); } else { throw new Exception("No POST data found."); } } else { // use provided data array $this->post_data = $post_data; foreach ($this->post_data as $key => $value) { $encoded_data .= "&$key=".urlencode($value); } } if ($this->use_curl) $this->curlPost($encoded_data); else $this->fsockPost($encoded_data); if (strpos($this->response_status, '200') === false) { throw new Exception("Invalid response status: ".$this->response_status); } if (strpos($this->response, "VERIFIED") !== false) { return true; } elseif (strpos($this->response, "INVALID") !== false) { return false; } else { throw new Exception("Unexpected response from PayPal."); } } /** * Require Post Method * * Throws an exception and sets a HTTP 405 response header if the request * method was not POST. */ public function requirePostMethod() { // require POST requests if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST') { header('Allow: POST', true, 405); throw new Exception("Invalid HTTP request method."); } } } ?>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With