PayPal IPN Updates to Accommodate HTTP1.1 - Connection reset by peer - ssl

We have been working to update our PayPal IPN scripts in accord with seom recent changes. Here is the instruction in part by PayPal ...
Your Action Required before February 1, 2013 You will need to update
your IPN and/or PDT scripts to use HTTP 1.1, and include the "Host:
www.paypal.com" and "Connection: close" HTTP headers in the IPN and
PDT scripts.
We did that and the IPNs failed. PayPal Merchant Technical Service asked that we move to a SLL connection. Previously our connection to PayPal had been based upon ...
fsockopen ('www.paypal.com', 80, $errno, $errstr, 30)
Now it is ...
fsockopen ('ssl://ipnpb.paypal.com', 443, $errno, $errstr, 30)
We had to overcome an SSL problem with our host config to get this working, but it now makes the connection. However IPN requires us to post back to PayPal in order to receive a "VERIFIED" notification, at which point the script can do it's local processing based upon a confirmed payment.
This is where it fails.
The lines of the sample PayPal script are:
if (!$fp) {
// HTTP ERROR
} else {
fputs ($fp, $header . $req);
while (!feof($fp)) {
$res = fgets ($fp, 1024);
if (strcmp (trim($res), "VERIFIED") == 0) { ... rest of script
But the response comes back blank. I have inserted a diagnostic to email me the value of $res at each stage. If I do this with the old script it comes back with a "VERIFIED" value just fine. The old scripts on several sites have been stable for years. I have now tried these new updates on two distinct sites on two different servers and the result is the same.
When looking at the Apache logs I see this error:
PHP Warning: fgets() [function.fgets]: SSL: Connection reset by peer in /home/[sitename]/public_html/[IPN scriptname].php on line 145
So it appears that PayPal shuts down the connection without sending a Verified response. However PayPal support deny that this is a problem at this end since the connection is opening in the first place.
I have spent several days trying to fix this. I still have the deadline of Feb 1st after which PayPal say my old scripts may not work, but neither do the new ones.
Anyone else have any experience with this?

If anyone else has the same issue, CURL seems to be the recommend choice for IPN by PayPal.
Check out there code sample on Github at: https://github.com/paypal/ipn-code-samples/blob/master/paypal_ipn.php

After struggle this issue a few hours. I got it right.
first, the header should be correct.
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Host: www.sanbox.paypal.com\r\n";
$header .= "Accept: */*\r\n";
$header .= "Connection: Close\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "\r\n";
second, the buffer should be big enough to receive whole msg from Paypal. the original sample code use 1024, but the result got truncated.
$res = stream_get_contents($fp, 2048);
After the two, the result return good.

Finaly found an answer! Paypal IPN Script, issue with feof and fgets
remove while(!feof($fp) ... and change it to $res = stream_get_contents($fp, 1024); also add $header .= "Conection: Close" to make sure that paypal closes the connection.

UPDATE: Jan 7th 2016
I noticed that the below works very well for tls://www.sandbox.paypal.com but when you go live to tls://www.paypal.com connection is refused! (It doesn't work) I found the problem is in the headers, sandbox and production level live paypal needs different headers! This is a bug but for you to get it work both in production level and sandbox, please use these headers respectively:
Production Level (Live PayPal Headers):
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
Sandbox PayPal Headers:
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Host: www.paypal.com\r\n";
$header .= "Accept: */*\r\n";
$header .= "Connection: Close\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "\r\n";
* The code below is the same and it works fine, just replace the headers respectively.*
Here is the rest of the code (and answer):
I checked all of the answers here and there to make it work in Jan 5th 2016.
All of them had good points, but didn't work for the whole picture.
So first, here is the complete code that actually works and then I'll go through it:
<?php
// =========================================================================
// PayPal Official PHP Codes and Tutorial:
// https://developer.paypal.com/webapps/developer/docs/classic/ipn/gs_IPN/
// =========================================================================
// Send an empty HTTP 200 OK response to acknowledge receipt of the notification
header('HTTP/1.1 200 OK');
// Assign payment notification values 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'];
// Build the required acknowledgement message out of the notification just received
$req = 'cmd=_notify-validate'; // Add 'cmd=_notify-validate' to beginning of the acknowledgement
foreach ($_POST as $key => $value) {
// Loop through the notification NV pairs
$value = urlencode(stripslashes($value)); // Encode these values
$req .= "&$key=$value"; // Add the NV pairs to the acknowledgement
}
// Set up the acknowledgement request headers
// HTTP POST request
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Host: www.sanbox.paypal.com\r\n";
$header .= "Accept: */*\r\n";
$header .= "Connection: Close\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "\r\n";
// Open a socket for the acknowledgement request
$fp = fsockopen('tls://www.sandbox.paypal.com', 443, $errno, $errstr, 30);
// Send the HTTP POST request back to PayPal for validation
fputs($fp, $header . $req);
// Log the transaction:
file_put_contents("paypal_post.txt", date('Y-m-d H:i:s')."\n".file_get_contents("php://input")."\n\n", FILE_APPEND);
// While not EOF
while (!feof($fp)) {
// Get the acknowledgement response
// $res = fgets($fp, 1024);
$res = stream_get_contents($fp, 1024);
$responses = explode("\r\n", $res);
foreach ($responses as $response) {
if (strcmp ($response, "VERIFIED") == 0) {
// Response contains VERIFIED - process notification
// Authentication protocol is complete - OK to process notification contents
// Possible processing steps for a payment include the following:
// Check that 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
} else if (strcmp ($response, "INVALID") == 0) {
// Response contains INVALID - reject notification
// Authentication protocol is complete - begin error handling
}
}
}
// Close the file
fclose($fp);
?>
OK, now you have the code that works to listen for a PayPal IPN, re-compile and send it back to PayPal for verification, receive the headers back, process the headers line by line to find the VERIFIED validation.
Which should be provided by PayPal all-in-one package in a working condition but their tutorial lacked very critical parts and it didn't work for me and many here on this thread.
So now what are the critical parts that makes it work:
First, the headers provided by paypal didn't work, I found #jp_eagle 's headers work perfectly.
And paypal was wrong for $res = fgets($fp, 1024); as well...
However you dont need $res = stream_get_contents($fp, 2048); as #jp_eagle suggested, $res = stream_get_contents($fp, 1024); is just fine.
while (!feof($fp)) {} loop should stay there to make it work! Yes, even with the switch from fgets() to stream_get_contents() it should stay there!
#richbai90 was wrong for the suggestion to remove it.
Try the other way and it won't work...
And the most critical part to get the VALIDATION is this loop:
$res = stream_get_contents($fp, 1024);
$responses = explode("\r\n", $res);
foreach ($responses as $response) {}
Now you can look for the VALIDATION in each line here:
if (strcmp ($response, "VERIFIED") == 0) {}
Also logged the initial complete incoming POST request form paypal before anything else:
// Log the transaction:
file_put_contents("paypal_post.txt", date('Y-m-d H:i:s')."\n".file_get_contents("php://input")."\n\n", FILE_APPEND);
That's it.
Now go here and login with your paypal to see if it works:
https://developer.paypal.com/developer/ipnSimulator/
Give your url select "cart checkout" for example and send a test IPN.
If it works all good, go ahead and remove the .sandbox from the above paypal URL from www.sandbox.paypal.com to www.paypal.com and you are live.
Fill in the VERIFIED and INVALID sections in the if-else statements with your database actions and it is done.

Related

How do I implement sendgrid.env file?

I am trying to implement the sendgrid email API.
I have followed all the instructions I can find to try and set up the sendgrid email API but I all I get is this:
Response status: 401 Response Headers - HTTP/1.1 401 Unauthorized
- Server: nginx - Date: Wed, 02 Mar 2022 21:56:45 GMT
- Content-Type: application/json
- Content-Length: 88
- Connection: keep-alive
- Access-Control-Allow-Origin: https://sendgrid.api-docs.io
- Access-Control-Allow-Methods: POST
- Access-Control-Allow-Headers: Authorization, Content-Type, On-behalf-of, x-sg-elas-acl
- Access-Control-Max-Age: 600
- X-No-CORS-Reason: https://sendgrid.com/docs/Classroom/Basics/API/cors.html
- Strict-Transport-Security: max-age=600; includeSubDomains
I installed the SendGrid helper library, ran the Composer command which created a composer.json file in the root of my project, installed the SendGrid helper library for PHP, along with its dependencies in a new directory named vendor.
I then used the following to create sendgrid.env:
echo "export SENDGRID_API_KEY='SG.XXX....XXXX'" > sendgrid.env
echo "sendgrid.env" >> .gitignore
source ./sendgrid.env
The API key works fine when I tested it using curl --request POST and email comes through ok.
But I have tried every combination I can think of to integrate the sendgrid.env e.g. changing the location, removing the single quotes etc. but I just get the same error message every time.
Here is my php script to send the email:
declare(strict_types=1);
require 'vendor/autoload.php';
use \SendGrid\Mail\Mail;
$email = new Mail();
// Replace the email address and name with your verified sender
$email->setFrom(
'paul#xxx.com',
'Paul xxx'
);
$email->setSubject('Sending with Twilio SendGrid is Fun');
// Replace the email address and name with your recipient
$email->addTo(
'paul#yyy.com',
'Paul yyy'
);
$email->addContent(
'text/html',
'<strong>and fast with the PHP helper library.</strong>'
);
$sendgrid = new \SendGrid(getenv('SENDGRID_API_KEY'));
try {
$response = $sendgrid->send($email);
printf("Response status: %d\n\n", $response->statusCode());
$headers = array_filter($response->headers());
echo "Response Headers\n\n";
foreach ($headers as $header) {
echo '- ' . $header . "\n";
}
} catch (Exception $e) {
echo 'Caught exception: '. $e->getMessage() ."\n";
}
This is the structure of the files:
My domain and email have been verified on Sendgrid.
I have a feeling that there's another step involved to get the API from the sendgrid.env file?
Thanks in advance.

Why does Telegram give me an empty POST request on the webhook?

I followed these steps:
Registered a bot with BotFather.
Send a post request for the webhook (https://api.telegram.org/bot[BOTID]/setWebhook) with the URL being https://example.com/myscript.php
Double check with getWebhookInfo and it showed it is correctly registered.
When I send a message to the bot, the script is being called but with an empty POST payload. In the documentation they say they would send an HTTPS POST request to the specified url, containing a JSON-serialized Update.
Does anyone else has this issue and perhaps know a way to resolve this?
My php script to log:
$file = dirname(__FILE__) . '/telegram-log.txt';
$entry = (object)array();
$entry->date = date("r");
$entry->GET = $_GET;
$entry->POST = $_POST;
$entry->REQUEST = $_REQUEST;
$entry->HTTP_USER_AGENT = $_SERVER['HTTP_USER_AGENT'];
$entry->REMOTE_ADDR = $_SERVER['REMOTE_ADDR'];
file_put_contents($file, json_encode($entry) . "\n", FILE_APPEND | LOCK_EX);
Response:
{"date":"Thu, 17 Jun 2021 13:42:49 +0200","GET":[],"POST":[],"REQUEST":[],"HTTP_USER_AGENT":null,"REMOTE_ADDR":"91.108.6.133"}
Use $content = file_get_contents("php://input") to receive updates.
Telegram's response is of content-type application/json.
$_POST will only work when using application/x-www-form-urlencoded or multipart/form-data as the HTTP Content-Type in the request.

XMLHttpRequest blocked by CORS Policy on axios.post

The axios.post (code below) must send data to url api/add-todo, but I get these errors:
axios.post('http://localhost/vueoctober/todo/api/add-todo', todo).then(function (response) {
console.log(response);
}).catch(function(error) {
console.log(error);
});
The route api/add-todo is handled with October method Route::get() (https://octobercms.com/docs/services/router). Why is it not found?
If I change axios.post to axios.get it will be working! But I need post data, not get.
What I tried:
1) I tried to add these headers to .htaccess:
Header add Access-Control-Allow-Origin "*"
Header add Access-Control-Allow-Headers "origin, x-requested-with, content-type"
Header add Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTION"
It's working only for axios.get. The axios.post is still blocking.
2) I added Header set Access-Control-Allow-Origin "*" to httpd.conf.
Vue app is serving at port 8080, therefore axios.post url can't be relative.
I also stumbled and struggled with this on FF, even though I have this in the .htaccess:
Header set Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTIONS".
After more searching I found a Gist by #marcomessa that fixed my issues.
https://gist.github.com/marcomessa/54d077a0208439f5340126ff629a3718
Look at the error message carefully, it says the response to the preflight request didn't have an HTTP ok status.
Clearly, your server-side code doesn't have a route handler for the OPTIONS request, so you need to add one.
As an aside, after the browser gets a successful OPTIONS response, it will make the POST request but you said:
The route api/add-todo is handled with October method Route::get()
You'll need to use Route::post() to handle that.
Hours of googling and I got answer...
1) Install plugin Cross-Origin Resource Sharing (CORS).
2) In htaccess of Vue app add:
Header set Access-Control-Allow-Origin '*'
Header set Access-Control-Allow-Headers "origin, x-requested-with, content-type"
Header set Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTION"
NOTICE! Write SET not ADD!
That's it.
So for clarification on this. There are always numerous ways to answer a problem. Here is what I did for mine. Check this for Preflight Request. The preflight request is created by the browser and is not for security. This function I created first will throw an okay message upon a request, then if the data contains any data it will then do what it is called (this is where you check for security). I don't have to mess with .htaccess files. Though I did install the CORS plugin because it is a nice plugin. Also the video from watch-learn does the author is making a cross-origin request in which he goes over how to correct the problem. I think he just filmed the video before preflight requests started to be a browser norm. Found routing information here.
Route::match(['POST', 'OPTIONS'],'api/update-todo', function(Request $req) {
$data = $req->input();
if (!empty($data)) {
Todo::where('id', $data['id'])
->update([
'name' => $data['name'],
'description' => $data['description'],
'status' => $data['status']
]);
return response()->json([
'Success' => $data,
]);
} else {
return response()->json([
'Success' => $req,
]);
}
});
I can not resolve it via axios, I wasted a lot of hours, but I resolved it very easy by this way.
Let's think we are posting:
{name:"Cynthia Merk", age:"22"}
I did the next function to send the last JSON (any JSON structure works):
const PostFunction = (data, letFunction, errorHandle) => {
let uri = "http://.../create.php";
let xhr = new XMLHttpRequest();
xhr.overrideMimeType("application/json");
xhr.open("POST", uri, true);
xhr.addEventListener("readystatechange", function() {
if(xhr.readyState === 4 && xhr.status === 200) {
letFunction(this.responseText);
}else{
errorHandle(this.responseText);
}
});
xhr.send(JSON.stringify(data));
}
You can invoke this function, it needs changes the "uri" variable value and it's required to use JSON.stringify to send the data.
In PHP the API is very easy too, for dummies:
<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: Origin, Content-Type, X-Auth-Token, X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE");
header("Allow: POST, GET, OPTIONS, PUT, DELETE");
class DB extends PDO {
private $host = 'localhost';
private $dbname = 'zamuSysScheme';
private $user = 'root';
private $password = 'admin';
private $charset = 'utf8';
public function __construct(){
try{
$dns = 'mysql:host=' . $this->host . ';dbname=' . $this->dbname;
parent::__construct($dns, $this->user, $this->password, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
} catch(PDOException $e){
echo 'Error: ' . $e->getMessage();
exit;
}
}
}
$method = $_SERVER['REQUEST_METHOD'];
$pdo = new DB();
if($_SERVER['REQUEST_METHOD'] == 'POST'){
$json = file_get_contents('php://input');
$decoded = json_decode($json, true);
try{
$Name = $decoded["name"];
$Age = $decoded["age"];
if(!isset($Name)){
header("HTTP/1.1 402 FAIL");
echo "The paramenter Name is not present";
exit;
}
if(!isset($Age)){
header("HTTP/1.1 403 FAIL");
echo "The paramenter Age is not present";
exit;
}
$sqlStatement = "INSERT INTO Client (Name, Age) VALUES";
$sqlStatement .= "(:Name, :Age)";
$stmt = $pdo->prepare($sqlStatement);
$stmt->bindValue(':Name', $Name, PDO::PARAM_INT);
$stmt->bindValue(':Age', $Age, PDO::PARAM_INT);
$stmt->execute();
$Client_Id = $pdo->lastInsertId();
if($Client_Id){
header("HTTP/1.1 200 OK");
echo $Pago_Id;
exit;
}
}catch(Exception $except){
header("HTTP/1.1 400 FAIL");
echo "Error: " . $json . " /// " . $except;
exit;
}
}
header("HTTP/1.1 401 BAD REQUEST");
?>
I hope it can help you, any question is allowed and if I have the answer I'll glad to help.

How can I configure Apigility to send HMAC-Authorization response headers?

I'm implementing an authorization in Apigility using Hmac. Basically I'm using the classe of this fella both for client and server (https://github.com/reinaldoborges/rb-sphinx-hmac-zf2-client).
Now I have this piece of code before sending a GET to my Api.
$hmac = new HMAC(
new HMACv0(),
new PHPHash('sha256'),
new StaticKey('[PRE-SHARED KEY]'),
new DummyNonce()
);
$hmac->setKeyId('certkey');
$hmac->setNonceValue('certNonce');
$uri = "http://apicert.local/certificados";
$cliente = new HMACHttpClient($uri);
$cliente->setMethod('GET');
$cliente->setHmac($hmac);
try {
$cliente->send();
/**
* Resposta
*/
echo "Request HMAC Header:", PHP_EOL; **I'm asking for a Reply using HMAC-Authorization type of header **
echo ' ', HMACHttpClient::HEADER_NAME, ' = ', $cliente->getHeader(HMACHttpClient::HEADER_NAME), PHP_EOL, PHP_EOL;
} catch (Exception $e) {
/**
* ERRO
*/
echo "##### ERRO #####", PHP_EOL;
echo $e->getCode(), ' : ', $e->getMessage(), PHP_EOL;
echo "##### ERRO #####", PHP_EOL, PHP_EOL;
}
$response = $cliente->getResponse();
echo "Response Status Code: ", $response->getStatusCode(), PHP_EOL, PHP_EOL;
echo "Response Headers: ";
print_r($response->getHeaders()->toArray());
echo PHP_EOL;
echo "Response Cookies:", PHP_EOL;
$cookies = $response->getCookie();
foreach ($cookies as $cookie) {
echo ' ', $cookie->toString(), PHP_EOL;
}
echo PHP_EOL;
echo "Response Body:", PHP_EOL;
echo $response->getBody();
echo PHP_EOL, PHP_EOL;
And I'm receiving a 406 Status Code that refers that this kind of header is not acceptable. I tried to add in content negotion whitelist but there is not this option. How can I receive the authorization from the server then?
Request HMAC Header: HMAC-Authentication = 1:certkey:certNonce:f9c9edcda43df5c466e54449f31900a6f9387afa32168327a0873a69b023abef Response Status Code: 406 Response Headers: Array ( [Date] => Thu, 11 Aug 2016 23:55:33 GMT [Server] => Apache/2.4.7 (Ubuntu) [Access-Control-Max-Age] => 1000 [Access-Control-Allow-Headers] => X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding [Access-Control-Allow-Methods] => POST, GET, OPTIONS, DELETE, PUT [Access-Control-Allow-Origin] => * [Content-Length] => 149 [Connection] => close [Content-Type] => application/problem+json ) Response Cookies: Response Body: {"type":"http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html","title":"Not Acceptable","status":406,"detail":"Cannot honor Accept type specified"}
You will have to create a new Authorization type.
In the APigility ui there is a button in the top named Authorization.
Here you can add any authorization adapter you want. Even the hmac adapter if you want.
Personally i have used this to create a jwt adapter.
Now, this adapter should extend ZF\MvcAuth\Authentication\AdapterInterface.
I've also added this adapter to my service manager.

PHP mail out late or when Apache server rebooted

I used to have php mail out right away, now however it might be changes in php code on page, or fact that i changed servers. Mail out like 10 minutes after user register, but if i go and restart apache it goes trough right after apache restart.
What settings should i tweak for php or apache to make it send right away again.
P.S. this is random issue sometimes it sends right away!
$from = "mail#otherdomain.com";
$email_address = $ex[0][Email_Address];
$ttc = "mymail#domain.com";
$eol = "\r\n";
$mime_boundary = md5(time());
$headers .= "From:".$from .$eol;
$headers .= "Reply-To:".$from.$eol;
$headers .= "Return-Path:".$from.$eol;
$headers .= "Bcc:".$ttc.$eol;
$headers .= "X-Mailer: PHP v".phpversion().$eol;
$headers .= "MIME-Version: 1.0".$eol;
$headers .= "Content-Type: text/html; charset=iso-8859-1".$eol;
if (preg_match("/gmail/",$to))
$headers = str_replace("\r\n","\n",$headers);
$result=mail($email_address,$subject,$mess,$headers);
header("location:home.html");