Background
I have an application that has a Wordpress plugin. I'm posting it here because AFAIK wp_remote_get() is basically a wrapper around CURL, and I don't think this is a Wordpress problem. The application (call it the back end) has a REST API. Most of the requests to the back end happen via ajax from javascript in the browser, and both Wordpress and the back end application have access to the same session - this is tested and working - the plugin has access to all the same session data that the back end does.
The problem
For some tasks the plugin needs to make requests to the back end directly (as in not ajax from the browser), so I need to pass the session cookie with those requests which are made via wp_remote_get(). Here is the code:
$headers = array(
'Content-Type' => 'application/json',
'Api-Key' => $options['api_key']
);
if ( !empty( $_SESSION[ 'Security' ][ 'accessToken' ] ) )
{
$headers[ 'Authorization' ] = 'Bearer ' . $_SESSION[ 'Security' ][ 'accessToken' ];
}
$cookies = [];
$cookie = new WP_Http_Cookie( 'main' );
$cookie->name = 'main';
$cookie->value = $_COOKIE['main'];
$cookie->expires = time()+120;
$cookie->path = '/';
$cookie->domain = '.example.com';
$cookie->samesite = 'none';
$cookie->secure = true;
$cookie->httponly = true;
$cookies[] = $cookie;
$request = wp_remote_get( $api_url, array(
'headers' => $headers,
'cookies' => $cookies
)
);
Now, I'm not 100% sure that this will give the back end access to the session as expected, because when I send this cookie along with the request I get an error:
[proxy_fcgi:error] [pid 1737121:tid 140694932940544] [client 192.168.1.1:40819] AH01071: Got error 'PHP message: https://example.com/shop/api/v3/item?id=100101
PHP message: fmldcke15n4uco3622i2t0nlj1
fmldcke15n4uco3622i2t0nlj1 is the current session_id(), which I find odd.
I have played with the various settings for the cookie, and the values being set are copied directly from session_set_cookie_params() from the back end.
So, 2 questions:
Why would sending a session cookie like this cause a hard error in fcgi?
Assuming I can get this to work, will this allow the back end to access the values in session when called in this fashion?
I'm trying to create a simple to use API with php but I have run into multiple problems. I'm working on an user api that does basic CRUD stuff.
If I do a POST request with cURL to my user api (user.php), a new session id is used during the execution of user.php.
So in order to combat that I tried to send the current session id with the POST request to user.php. The problem I have right now is that after setting the id with session_id($_POST['session']) and then starting my session with session_start() my server will get stuck on executing that code and will eventually throw an Internal Server Error 500.
I tried to get my server to show me what the error is with ini_set('display_errors', 1); and an .htaccess file (content: php_flag display_errors 1) but to no avail, the server just gets stuck.
test.php
<?php
session_start();
// Check for available session
if (!isset($_SESSION['id'])) {
header('location: index.php');
} else {
// Initialize cURL
$curl = curl_init();
// Set parameters for POST request
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_URL => 'http://localhost/api/user.php',
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query([
'session' => session_id(),
'username' => 'testuser',
'password' => 'testalot',
'name' => 'testuser',
'isAdmin' => 0
])
]);
// Execute POST request
$response = curl_exec($curl);
/* <<< Doesn't get beyond this point. */
// Dump JSON
var_dump($response);
// Close cURL session
curl_close($curl);
}
?>
user.php
<?php
// Declare integer checking function
function isInteger($input) {
return ctype_digit(strval($input));
}
// Declare result object
$output = ['success' => false, 'data' => [], 'error' => ''];
// Action on POST
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Check if session id was sent
if (!empty($_POST['session'])) {
// Set session id
session_id($_POST['session']);
// Start session with received session id
session_start();
/* <<< Freezes at this point. */
// Check session for available user id
if (isset($_SESSION['id'])) {
// Only admins are allowed to execute POST requests
if ($_SESSION['isAdmin'] == 1) {
// ... more code ...
I'd of course also use a different method of accessing my API that does not require me to send the current session id, if there is any. Any ideas?
UPDATE:
Look #Neok's comment under my question. That is the solution. Just pointing it out for others that might have the same issue.
Make sure that php.ini is set to log all errrors
error_reporting(E_ALL);
or
ini_set('error_reporting', E_ALL);
PHP Documentation says session_id() returns the session id for the current session or the empty string ("") if there is no current session (no current session id exists). So instead of taking $_SESSION['id'], try that:
// Check for available session
if (session_id() == "") {
header('location: index.php');
} else {
...
}
Otherwise your code redirects to index.php in a loop causing error 500.
Accessing the Coinbase API used to be really simple: all you needed was an API key. Now you need something called a "nonce" and a "signature". I pass my new API "Secret", the nonce, and the Key in my request, but it returns an "invalid_key" error. What gives?
EDIT March 12th: Added a tutorial on interacting with the API via OAuth.
The fact that the API used to be so simple -- only needing a Key -- means it was pretty insecure. So they beefed up the security a week-ish ago. Here's the blog post:
http://blog.coinbase.com/post/75936737678/more-security-and-granular-control-with-the-new-api
Everyone now gets an API "Secret" in addition to an API Key. Whenever you make a request to the API, you have to include three parameters:
Your API Key.
A "nonce", which is a unique number that you use to identify something. In this case, every single request you make needs to have a new number, and each request's nonce has to be bigger than the one before it.
Your API "Signature". This is NOT your API "Secret".
The Signature is your nonce followed immediately by the full URL to which you're posting your request, parameters and all. This URL also contains the nonce, so the whole thing all together would look something like this:
12345https://coinbase.com/api/v1/buttons?nonce=12345&name=Socks&price=9.95
Then you take that whole thing and encode it as a "SHA256" hash. If you don't know what that means, don't panic -- you can do it in one line using a function PHP already has built in.
At any rate, I was having some trouble figuring all this out, so I spent a little while on it and put together this script, which makes GETing and POSTing to the API really easy. I'd love to hear people's thoughts!
<?php
function coinbaseRequest($what,$getOrPost,$parameters){
//Obviously, your API Key and Secret go here.
$apikey = "blahblahblah";
$apisecret = "blahblahblahblah";
$nonce = file_get_contents("nonce.txt") + 1;
file_put_contents("nonce.txt", $nonce, LOCK_EX);
$url = "https://coinbase.com/api/v1/" . $what . "?nonce=" . $nonce;
if($parameters != ""){
$parameters = http_build_query(json_decode($parameters), true);
}
//Here I go, hashing the Signature! Thanks, PHP, for making this easy!
$signature = hash_hmac("sha256", $nonce . $url . $parameters, $apisecret);
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array(
"ACCESS_KEY: " . $apikey,
"ACCESS_NONCE: " . $nonce,
"ACCESS_SIGNATURE: " . $signature
)));
if($getOrPost == "post"){
curl_setopt_array($ch, array(
CURLOPT_POSTFIELDS => $parameters,
CURLOPT_POST => true,
));
}
$results = curl_exec($ch);
curl_close($ch);
echo $results;
}
//This is a POST example.
coinbaseRequest("buttons", "post", '{
"button": {
"name": "test",
"price_string": "1.23",
"price_currency_iso": "USD",
"variable_price": true
}
}');
//This is a GET example.
coinbaseRequest("account/balance", "get", false);
?>
Notes:
I tried using (microtime(true)*100) for my nonces. The problem is it makes a decimal number, and the last few digits kept getting dropped or rounded off so it didn't work. Then I thought, "Screw this", made a blank nonce.txt file, and wrote 1 in it, and to get nonces I just got the contents of that file, added 1, and replaced the file with the new number. It served a second purpose as a counter showing how many total requests I've made.
But then someone pointed out to me PHP's "uniqid" function, which generates an ID based on the current microtime. So you can also try this:
$nonce = hexdec(uniqid());
This has the advantage of not accessing an external file. I actually really like being able to see how many requests I've made, and so will probably stick with the (bad) nonce.txt method.
The coinbaseRequest() function has three parameters. The first is the directory to which you're making your request -- that is, whatever's supposed to come after "https://coinbase.com/api/v1/". The second parameter is "get" or "post", depending on whether it's a GET or a POST request. (Make sense?)
The third parameter is all the queries you're passing in your request. This should be formatted as JSON, unless it's a GET request that doesn't take any parameters (besides the Key, Nonce, and Signature which the function includes for you), in which case you should leave this as false.
EDIT, March 3rd:
I made a little function for taking whatever's returned by coinbaseRequest and turning it into a button:
function makebutt($data){
$data = json_decode($data,true);
$buttoncode = $data["button"]["code"];
return ("<a class=\"coinbase-button\" data-code=\"" . $buttoncode . "\" href=\"https://coinbase.com/checkouts/" . $buttoncode . "\">Pay With Bitcoin</a><script src=\"https://coinbase.com/assets/button.js\" type=\"text/javascript\"></script>");
}
YE GREAT OAUTH TUTORIAL
<?php
/*OAuth is great. It's also complicated. Or rather, it LOOKS complicated.
This whole script is just one big long function. It's a really, really ugly
function. I broke down everything "Barney-style" to demonstrate all the steps
in the process, and because there are some things you have to decide -- how to
record the user data, for instance.
Let's get this train wreck a rollin'.*/
function oauthRequest($apiPath,$getOrPost,$parameters){
/*You get this info from https://coinbase.com/oauth/applications*/
$clientId = "#####";
$clientSecret = "#####";
$callbackUrl = "http://www.blah.com/oauth.php";
function curling($url,$getpost,$params){
if($params != ""){
$params = http_build_query(json_decode($params), true);
}
if($getpost == "get"){
$ispost = false;
$url .= $params;
}
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true
));
if($getpost == "post"){
curl_setopt_array($ch, array(
CURLOPT_POST => $ispost,
CURLOPT_POSTFIELDS => $params
));
}
$results = curl_exec($ch);
curl_close($ch);
return $results;
}
/*There are two people involved here: the Client (you), and the User (the
person accessing your app or site).
You'll need 3 pieces of data for each user before your app can access their
Coinbase account: a User Code, an Access Token, and a Refresh Token.
For the purposes of this demonstration, I'm recording all of the user data in
a .txt file on my server. THIS IS PROBABLY A BAD IDEA in real life because .txt
files aren't secure at all, and this text file will only store the data for one
user (it gets overwritten every time). This is the kind of stuff you'd put in an
SQL database if you have one, or maybe in the user's cookies.*/
if(!file_exists("userdata.txt") || file_get_contents("userdata.txt") == ""){
file_put_contents("userdata.txt",json_encode(array(
"userCode" => "",
"accessToken" => "",
"refreshToken" => ""
)), LOCK_EX);
}
$userData = json_decode(file_get_contents("userdata.txt"), true);
/*Okay. So. First thing we're going to do is see if we have a User Code for
this user. These are big alphanumeric strings that are 64 characters long. If
we have one, it'll either be in the URL of this page (the $_GET array), or
it'll be in that userdata.txt file.*/
if(array_key_exists("code",$_GET) && $_GET["code"] != ""){
$userCode = $_GET["code"];
}else if(array_key_exists("userCode",$userData) && $userData["userCode"] != ""){
$userCode = $userData["userCode"];
}else{
/*If we don't have a User Code, then this next snippet of code will fire. It'll
return the link for a special user-specific Coinbase page to which the user
will need to go to authorize your app to access their Coinbase account (by
signing into Coinbase and clicking a green "Authorize" button).
After authorizing your app, they'll be automatically taken to the Redirect URL
you specified, with their User Code added to the end of the URL. So if your
Redirect URL is www.blah.com/oauth.php, they'll go to www.blah.com/oauth.php?
code=123451234512345 .
This User Code never expires, and so theoretically the user should only need to
go to the authorization link once. However, if you don't make a way of getting
the User Code in the future (my fancy "userdata.txt" in this case) or they de-
authorized your app from within their Coinbase account, then they'll need to go
to the link again and re-authorize your app from the beginning.
I have it set up so my Redirect URL and the rest of my OAuth script are all on
the same page: www.blah.com/oauth.php . So the user will just start back at the
beginning of this script, and THIS time the script will see the User Code in
the URL (the $_GET array), and so will skip this next bit.
Whew. You with me so far?*/
return ("https:/*coinbase.com/oauth/authorize?" . http_build_query(array(
"response_type" => "code",
"client_id" => $clientId,
"redirect_uri" => $callbackUrl
)));
die;
}
/*Here I am, recording the User Code for future use in userdata.txt*/
$userData["userCode"] = $userCode;
file_put_contents("userdata.txt",json_encode($userData),LOCK_EX);
/*Alright, we've got the User Code. Now we need the Access Token -- another 64-
character string. The difference is that the Access Token expires every 2 hours
(7200 seconds). Let's see if we already have one in the userdata.txt file.*/
if(array_key_exists("accessToken",$userData) && $userData["accessToken"] != ""){
$accessToken = $userData["accessToken"];
$refreshToken = $userData["refreshToken"];
}else{
/*If we're here, it's because we don't have an Access Token for this user. We
get one by making this POST request:*/
$authorization = json_decode(curling(
"https:/*coinbase.com/oauth/token" . "?" . http_build_query(array(
"grant_type" => "authorization_code",
"code" => $userCode,
"redirect_uri" => $callbackUrl,
"client_id" => $clientId,
"client_secret" => $clientSecret
)), "post", ""), true);
if(array_key_exists("error",$authorization)){
/*If something goes wrong here, I'm going to clean out userdata.txt and ask the
user to try again.*/
file_put_contents("userdata.txt","",LOCK_EX);
die("Something went wrong. Please refresh the page and try again.");
}
$accessToken = $authorization["access_token"];
$refreshToken = $authorization["refresh_token"];
}
/*The Refresh Token is what you use to get a new Access Token once the current
Access Token has expired. The Refresh Token never expires, but can only be used
once. Anytime you get an Access Token, you'll also be given a Refresh Token.
If you don't have the Refresh Token and a working Access Token for the user,
they'll need to re-authorize your app all over again.
I'm backing up the Access Token and Refresh Token to userdata.txt*/
$userData["accessToken"] = $accessToken;
$userData["refreshToken"] = $refreshToken;
file_put_contents("userdata.txt",json_encode($userData),LOCK_EX);
/*Alright! At this point, we should have the three bits of user data we need:
the User Code, the Access Token, and the Refresh Token. So now lets try
actually making an API request.
This whole script is really just one big function called "oauthRequest". You
pass three parameters to the function: the path of the API request (everything
after https:/*coinbase.com/api/v1/), whether this API query is a GET or a POST,
and any parameters that go along with that GET or POST request. These params
first come into play here.
Let's make the API request:*/
$results = curling("https:/*coinbase.com/api/v1/" . $apiPath . "?" . http_build_query(array(
"access_token" => $accessToken
)), $getOrPost, $parameters);
/*Now we're going to make sure the request actually worked, and didn't get
rejected because the Access Token was expired. If it WAS expired, the
results should be blank. (It'll return a 401 if you want to get fancy.)*/
$resultsArray = json_decode($results);
if(count($resultsArray) < 1){
/*Looks like it did expire, so now we make a POST request using the Refresh
token, which will return a new Access Token AND a new Refresh Token.*/
$reAuthorization = json_decode(curling(
"https:/*coinbase.com/oauth/token?" . http_build_query(array(
"grant_type" => "refresh_token",
"code" => $userCode,
"refresh_token" => $refreshToken
)), "post", ""), true);
$accessToken = $reAuthorization["access_token"];
$refreshToken = $reAuthorization["refresh_token"];
/*Let's back those up to userdata.txt...*/
$userData["accessToken"] = $accessToken;
$userData["refreshToken"] = $refreshToken;
file_put_contents("userdata.txt",json_encode($userData),LOCK_EX);
/*...and try the API request all over again:*/
$results = curling("https:/*coinbase.com/api/v1/" . $apiPath . "?" . http_build_query(array(
"access_token" => $accessToken
)), $getOrPost, $parameters);
/*If it doesn't work THIS time, I'm going to clean out userdata.txt and ask
the user to try again. One of the codes probably got all mungled up.*/
$resultsArray = json_decode($results);
if(array_key_exists("error",$resultsArray)){
file_put_contents("userdata.txt","",LOCK_EX);
die("Something went wrong. Please refresh the page and try again.");
}
}
/*If, however, everything went right, then this function will return the JSON
string with the data from the API! Hooray!*/
return $results;
}
/*Here are 4 different example requests you can make.*/
/*
echo oauthRequest("account/generate_receive_address","post","");
echo oauthRequest("buttons","post",'{
"button": {
"name": "test",
"type": "buy_now",
"price_string": ".01",
"price_currency_iso": "USD"
}
}');
echo oauthRequest("prices/buy","get",'{
"qty": 1,
"currency": "USD"
}');
echo oauthRequest("account/balance","get","");
*/
?>
It's not working because Coinbase recently implemented the OAuth2 protocol. This ensures the personal information of your user is transmitted securely. I referred to this implementation several months ago when writing my own OAuth class on another project:
I'm sending multiple requests to same client. zend_Http_Client not able to redirect because our site giving a javascript redirect. And from that javascript I'm getting the redirect url and again calling the client , now I'm getting access denied. This is beacuse I'm not able to store the session from the first client request.
I'm using the following code..
$client = new Zend_Http_Client(
$loginUrl,
array(
'keepalive' => true,
'timeout' => 1000
)
);
$response = $client->request()->getBody();
$redirectUrl = $this->_getResponseRedirectUrl($response);
$client->resetParameters();
$client->setUri($redirectUrl);
$response = $client->request()->getBody();
$resultUrl = $this->_getResponseRedirectUrl($response);
Can anybody tell me how to store the session for the second request.
Before trying the following steps make sure your server authentication is based on session cookies.
Solution Worked for me:
You need to add cookies from the first repose to the second request. In this case server consider your second request as authenticated one.In the response after login to the site your cookie will be having following set of values like cookie-name,cookie-value,Expiration Date, Path, Secure and HTTPOnly. From the cookie string explode cookie-name and cookie-value
Example Session Cookie after login response:
"JSESSIONID=DQl3NKXXmy3yntp3NW2GMlcn8pLn9PR9rl0lnR6vbtfdVpyHWdnq!598502565; path=/"
"CK_LanguageID_252085=1; expires=Friday, 23-Mar-2012 05:31:03 GMT; path=/; secure"
"CK_TimeZone_252085=4; expires=Friday, 23-Mar-2012 05:31:03 GMT; path=/; secure"
Create new cookie string for further server communication in the following pattern:
"JSESSIONID=DQl3NKXXmy3yntp3NW2GMlcn8pLn9PR9rl0lnR6vbtfdVpyHWdnq!598502565; CK_LanguageID_252085=1; CK_TimeZone_252085=4"
Adding a handy method to create cookie string from zend_http_client "Set-cookie" array.
/**
* Get clean cookie string with only name,value pair
* This method filer all the follwoing cookie information
* Expiration Date, Path, Secure and HTTPOnly
* #access public
* #param {Array} $cookies
* #return {String} $cookieString
*/
public function getCookieString(array $cookies){
$cookieString = null;
foreach($cookies as $cookie){
$part = explode(';',$cookie);
$cookieString = ($cookieString == null)? $part[0] : $cookieString . '; ' . $part[0];
}
return $cookieString;
}
Using Zend_Http_Client making consecutive requests:
//Login
$client = new Zend_Http_Client($loginUrl);
$response = $client->request();
//Get Header from response
$headers = $response->getHeaders();
//Create second header
$header = array("Cookie" => $this->getCookieString($headers["Set-cookie"]));
$client->setHeaders($header);
//Second request
$client->setUri($redirectUrl);
$response = $client->request();
Here I am removing "$client->resetParameters();" because you are not setting any GET Params using "$client->setParameterGet()"(Same for POST as well)
If using "$client->setParameterGet()" or "$client->setParameterPost()" use "$client->resetParameters();" before setting second uri.
$client->resetParameters() accepts boolean values:
FALSE : This is the default value which reset POST and GET Params only.
TRUE : Reset all the params including headers,last request and last response.
Actuall, Zend_Http_Client provides a very simple way to do what you've done, no need to reinvent the wheel here.
$client = new Zend_Http_Client(
$loginUrl,
array(
'keepalive' => true,
'timeout' => 1000
)
);
// this is the magic line...
$client->setCookieJar();
$response = $client->request()->getBody();
$redirectUrl = $this->_getResponseRedirectUrl($response);
$client->resetParameters();
$client->setUri($redirectUrl);
$response = $client->request()->getBody();
$resultUrl = $this->_getResponseRedirectUrl($response);
Read more here: http://framework.zend.com/manual/en/zend.http.cookies.html