I am stuck generating the OAuth signature programmatically.
I am on FreeBSD with PHP71 and can not use the PECL extension, because it doesn't work with PHP > 5.6 yet.
I read the RFC and many many SO threads as well as blogs that all have the same problem I have.
But I can't figure out where I failed.
I am trying to obtain a request token from the XING.com Api.
My Code:
$strConsumerKey = '12345';
$strConsumerSecret = '1234567890';
$arrQueryParams = [
'oauth_callback' => 'http://xing.dev/endpoint?hauth.done=XING',
'oauth_consumer_key' => $strConsumerKey,
'oauth_nonce' => md5(microtime() . mt_rand()),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0'
];
uksort($arrQueryParams, 'strcmp');
$strNormalizedParams = [];
foreach($arrQueryParams AS $k => $v)
$strNormalizedParams[] = $k . '=' . $v;
$strRequestTokenUrl = 'https://api.xing.com/v1/request_token';
$strBaseString = 'GET&' . rawurlencode($strRequestTokenUrl) . '&' . rawurlencode(implode('&', $strNormalizedParams));
$strKey = rawurlencode($strConsumerSecret) . '&';# . rawurlencode($strConsumerKey);
$strOAuthSignature = base64_encode(hash_hmac('sha1', $strBaseString, $strKey, true));
$arrQueryParams['oauth_signature'] = $strOAuthSignature;
uksort($arrQueryParams, 'strcmp');
$params = [];
foreach($arrQueryParams AS $k => $v)
$params[] = $k . '=' . rawurlencode($v);
$strFinalRequest = $strRequestTokenUrl . '?' . implode('&', $params);
$ch = curl_init($strFinalRequest);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$mixResponse = curl_exec($ch);
var_dump($mixResponse);
curl_close($ch);
exit;
I tried it as GET/POST request (Api Docs say both is possible)
I tried all combinations in creating the $strKey, but I think the current is the correct one
I checked my system time, I tried different timezones
$strBaseString example:
GET&https%3A%2F%2Fapi.xing.com%2Fv1%2Frequest_token&oauth_callback%3Dhttp%3A%2F%2Fxing.dev%2Fendpoint%3Fhauth.done%3DXING%26oauth_consumer_key%3D12345%26oauth_nonce%3Daadab05f87028514358e5995dc6728fd%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1503318691%26oauth_version%3D1.0
Final Request example:
https://api.xing.com/v1/request_token?oauth_callback=http%3A%2F%2Fxing.dev%2Fendpoint%3Fhauth.done%3DXING&oauth_consumer_key=12345&oauth_nonce=71f83f72b929a87e5fdd6bf1a7fb2511&oauth_signature=56qc3%252FCrMptnUoy6TgsJtsZclfY%253D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1503318773&oauth_version=1.0
The response is always:
{"message":"Invalid OAuth signature","error_name":"INVALID_OAUTH_SIGNATURE"}
The real funny think is though, that the https://github.com/xing/xing-api-samples/tree/master/php XING client works on my machine. I debugged the code and looked how they are generating the signature, dumped out the generated base string, the final string, params, etc. and everything looks like what I do. (Except for the values of oauth_timestamp and oauth_nonce of course)
Also I can reproduce the results on the oauth example: https://oauth.net/core/1.0/#sig_base_example
This makes it even more strange.
Has anyone an idea what I am doing wrong?
Thank you and best regards
Found the mistake:
$strBaseString = 'GET&' . rawurlencode($strRequestTokenUrl) . '&' . rawurlencode(implode('&', $strNormalizedParams));
should be
$strBaseString = 'GET&' . rawurlencode($strRequestTokenUrl) . '&' . rawurlencode(http_build_query($arrQueryParams));
Related
I've just started to upgrade my Google Cloud Storage code from API version 1.0 to version 2.0 and I'm having some troubles.
With version 1.0 I used Signed URLs with great success, using .p12 files. However that's deprecated in the new version and I have to use Firebase/php-jwt instead, using JSON files.
The problem is that it's just not working, I get the error:
<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message>
<StringToSign>PUT
image/png
1483626991
/myBucket/folder/test.PNG</StringToSign></Error>
This is the simplified code used to sign it.
$string = ($method . "\n" .
$contentMd5 . "\n" .
$contentType . "\n" .
$expiration . "\n" .
$file);
$signedURL = base64_encode(Firebase\JWT\JWT::encode($string,
file_get_contents($credentialsFilePath)));
After the signedURL is received I build an URL with the correct data. The only part I've changed from 1.0 and 2.0 is the part where you sign the URL. Furthermore I've checked that the string in "StringToSign"-field of the response is exactly the same as the one I'm signing.
In version 1.0 I signed the URL like this:
$signedURL = base64_encode((new Google_Signer_P12(
file_get_contents($p12FilePath),
'notasecret'
))->sign($string));
All of this leads me to believe that I'm singing the correct contents but using the JWT function the wrong way. Has anyone else done this? How did yo do it?
In case it's interesting this is the URL I build (works with 1.0):
$returnArr['url'] = "https://{$bucket}.commondatastorage.googleapis.com/"
. $prefix . '/' . rawurlencode($file)
. "?GoogleAccessId=" . rawurlencode($serviceEmail)
. "&Expires={$expiration}"
. "&Signature=" . rawurlencode($signature);
Looking at the source for that JWT library the first thing that jumps out at me, and I see was noted in comments, is that your payload should be an array or object, not a string... "JSON web tokens".
* #param object|array $payload PHP object or array
public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
Second, it looks like you are double base64 encoding it... base128? :)
The return value of encode should be the three Base64url strings concatenated together, so you shouldn't need to do it again.
I'd give this a try:
$payload = ['HTTP_Verb' => $method,
'Content_MD5' => $contentMd5,
'Content_Type' => $contentType,
'Expiration' => $expiration,
'Canonicalized_Resource' => $file];
$key = file_get_contents($credentialsFilePath);
$signedURL = Firebase\JWT\JWT::encode($payload, $key); //think base64_encode here is redundant.
Ref: Overview of Signed URLs page. They sure don't explain things very well in those docs.
I assume you've looked at SDK?
If you wanted to go the string route you would need to sign using RSA signatures with SHA256... opensssl_sign or also maybe easier to lean on Google's PHP SDKs?
Later...
OK, decided to test it. Saw Google Cloud had a free trial. Installed gsutil, read a bunch of docs. Damned if I understand this JWT approach though. Share if anyone can even provide the docs on that topic.
This code works:
<?php
$method = 'GET';
$expires = '1503532674';
$container = '/example-bucket/cat.jpeg';
$payload = "{$method}\n\n\n{$expires}\n{$container}";
//assume you have this 'json' formatted key too? Otherwise just load the private key file as is.
$key = file_get_contents('~/oas_private_key.json');
$key = json_decode($key, true);
$key = $key['private_key'];
//if sucessful the encypted string is assigned to $signature
openssl_sign($payload, $signature, $key, OPENSSL_ALGO_SHA256);
$signature = urlencode(base64_encode($signature));
die("https://storage.googleapis.com/{$container}?GoogleAccessId=oastest#foo.iam.gserviceaccount.com&Expires={$expires}&Signature={$signature}");
Finally no "SignatureDoesNotMatch" error! Personally I'd use the SDK. Little bit of init and you can just do something like the following:
$url = $object->signedUrl(new Timestamp(new DateTime('tomorrow')), [
'method' => 'PUT'
]);
It would also make upgrades easier in the future.
I have the PHP application integrated with Dropbox API. It worked good but since some time ago it stopped to work with non-latin symbols.
For example, if i try to create a dropbox folder with the API and use Cyrillic letters in folder name then API requests fail with error {"error": "Unauthorized"}
I have the PHP function
function createOAuthSignature($apiurl, $params, $oauth_secret, $oauth_token_secret, $method='GET'){
$urlencoded_apiurl = urlencode($apiurl);
$urlencoded_params = urlencode($params);
$basestring = $method .'&'.$urlencoded_apiurl.'&'.$urlencoded_params;
$oauth_signature = base64_encode(hash_hmac('sha1', $basestring, $oauth_secret.'&'.$oauth_token_secret, true));
$oauth_signature = urlencode($oauth_signature);
return $oauth_signature;
}
And the calling code is
$fullname = $parent . $foldername;
$fullnameEnc = rawurlencode($fullname);
$apiurl = $this->api_url .'fileops/create_folder/';
$params = 'oauth_consumer_key='.$this->oauth_consumer_key
.'&oauth_nonce='. $this->createNonce()
.'&oauth_signature_method=HMAC-SHA1'
.'&oauth_timestamp='. time()
.'&oauth_token='.$oauth_token
.'&oauth_version=1.0'
.'&path='. $fullnameEnc
.'&root=dropbox';
$oauth_signature = $this->createOAuthSignature($apiurl, $params, $this->oauth_secret, $oauth_token_secret);
$params .= '&oauth_signature='.$oauth_signature;
$action = $apiurl .'?'. $params;
$s = $this->curl->get($action);
What can be the problem there? I presume it is related to signature generating. But what is wrong there? This code worked fine just couple weeks ago.
Thanks
As Greg mentioned in a comment above, we're investigating, but if you want to switch to PLAINTEXT signing, it's quite easy. Change createOAuthSignature to just do this:
function createOAuthSignature($apiurl, $params, $oauth_secret, $oauth_token_secret, $method='GET'){
return urlencode($oauth_secret.'&'.$oauth_token_secret);
}
And in the calling code, change the signature method to PLAINTEXT:
$params = ...
.'&oauth_signature_method=PLAINTEXT'
...
I am trying to work with the PHP API for Amazon's Flexible Payments.
Here is my PHP snippet to send a payment request:
<?php
$string_to_sign = 'GET
authorize.payments-sandbox.amazon.com
/cobranded-ui/actions/start
SignatureMethod=HmacSHA256&SignatureVersion=2&callerKey=my_access_key&callerReference=YourCallerReference&paymentReason=donation&pipelineName=SingleUse&returnUrl=http%3A%2F%2Fproblemio.com&transactionAmount=4.0';
$encoded_string_to_sign = URLEncode(Base64_Encode(hash_hmac('sha256', $string_to_sign, 'my_secret_key')));
$amazon_request_sandbox = 'https://authorize.payments-sandbox.amazon.com/cobranded-ui/actions/start?SignatureVersion=2&returnUrl='.$return_url.'&paymentReason='.$payment_reason.'&callerReference=YourCallerReference&callerKey='.$my_access_key_id.'&transactionAmount=4.0&pipelineName=SingleUse&SignatureMethod=HmacSHA256&Signature='.$encoded_string_to_sign;
// When it goes to the url, it gets the invalid signature error
header('Location: '.$amazon_request_sandbox);
?>
This seems to be following their instructions, but I can't get past that error.
Thanks!!
<?php
$method = 'GET';
$host = 'authorize.payments-sandbox.amazon.com';
$path = '/cobranded-ui/actions/start';
$params = array(
'signatureMethod' => 'HmacSHA256',
'signatureVersion' => '2',
'currencyCode' => 'USD',
'callerKey' => 'Your_Key_ID',
'callerReference' => 'YourCallerReference',
'paymentReason' => 'donation',
'pipelineName' => 'SingleUse',
'returnUrl' => 'http://yourcallback.com',
'transactionAmount'=> '5',
'version' => '2009-01-09',
);
$params = array_map('rawurlencode', $params);
$paramStringArray = array();
foreach($params as $key => $value){
$paramStringArray[] = $key . '=' . $value;
}
$paramString = implode('&', $paramStringArray);
$string_to_sign = $method . "\n"
. $host . "\n"
. $path . "\n"
. $paramString;
$signature = base64_encode(hash_hmac(
'sha256',
$string_to_sign,
'Your_Super_Secret_Key',
true
));
$amazon_request_sandbox = "https://{$host}{$path}?" . $paramString .
'&signature=' . rawurlencode($signature);
header('Location: '.$amazon_request_sandbox);
?>
Okay... using the structure from the code below, I've finally figured this whole thing out via the code above. There are three things of note to keep track of while forming your signature/URL...
It seems that the parameter "transactionAmount" is necessary for a valid Co-branded UI Pipeline, even though there's no specific instruction alluding to the issue.
If any of your parameters have/had spaces in them, and you tried to use html_build_query() in all but the latest (5.4) version of PHP, you would be given an encoding scheme that featured "+" marks for spaces instead of "%20" which is what Amazon appears to like. My code above takes care of that by implementing rawurlencode() on the entire parameter array.
The ordering of the parameters is paramount in the construction of the signature. The keys (not the values) need to be in case-insensitive alphabetical order. It's also worth noting that despite what the documentation says for the API, both the ampersands (&) and the equals (=) must be present in the creation of the query string for the signature.
Ex:
Query String for Signature: callerKey=1111111111111¤cyCode=USD&signatureVersion=2
Some Other Things I Noticed...
In the sample code included with the PHP SDK (2010-8-28), the "paymentReason" attribute in the file "CBUISingleUsePipelineSample.php" is listed as "HarryPotter 1-5 DVD set". Since this attribute has spaces in it, it throws that ever-annoying "invalid signature" error when you try to visit the generated link because html_build_query() is used to generate the query string for the URL. To fix this issue, open up "CBUIPipeline.php", and look for the following line in the constructUrl() method...
$queryString = http_build_query($parameters, '', '&');
replace it with:
$queryString = str_replace('+', '%20', http_build_query($parameters, '', '&'));
That'll solve the space-encoding problem for older versions of PHP (< 5.4). With the latest version, there's an "enc_type" flag you can set.
Last things Last...
This is my first post on StackOverflow so don't kill me if I broke protocol. Hope it helps!
Try this piece of code:
<?php
$method = 'GET';
$host = 'authorize.payments-sandbox.amazon.com';
$path = '/cobranded-ui/actions/start';
$params = array(
'SignatureMethod' => 'HmacSHA256'
'SignatureVersion' => 2,
'callerKey' => 'my_access_key',
'callerReference' => 'YourCallerReference',
'paymentReason' => 'donation',
'pipelineName' => 'SingleUse',
'returnUrl' => 'http://problemio.com&transactionAmount=4.0',
);
$string_to_sign = $method . "\n"
. $host . "\n"
. $path . "\n"
. http_build_query($params);
$signature = base64_encode(hash_hmac(
'sha256',
$string_to_sign,
'my_secret_key'
));
$params['Signature'] = $signature;
$amazon_request_sandbox = "https://{$host}{$path}?" . http_build_query($params);
header('Location: ' . $amazon_request_sandbox);
So I made a few changes:
PHP's http_build_query() to build the query string (ensure correct encoding)
trying to re-use your vars vs. duplicating the efforts (makes it easier to spot mistakes, etc.)
explicit \n - maybe your editor entered \r or \r\n
HTH
Ive cracked oAuth and have my class file for it. I'm at the last stage of posting a tweet and all works except all the words are joined with a plus sign in the tweet.
Changing anything results in the signature been incorrect and twitter returns 401 error.
So how does one remove the pluses? Post function below:
function post($token, $tokenSecret, $status)
{
// Default params
$params = array(
"oauth_version" => "1.0",
"oauth_nonce" => time(),
"oauth_timestamp" => time(),
"oauth_consumer_key" => $this->key,
"oauth_signature_method" => "HMAC-SHA1",
"oauth_token" => $token,
"status" => $status
);
uksort($params, 'strcmp');
// convert params to string
foreach ($params as $k => $v) {$pairs[] = $this->_urlencode_rfc3986($k).'='.$this->_urlencode_rfc3986($v);}
$concatenatedParams = implode('&', $pairs);
// form base string (first key)
$baseString= "POST&".$this->_urlencode_rfc3986($this->request_statuses_url)."&".$this->_urlencode_rfc3986($concatenatedParams);
// form secret (second key)
$secret = $this->_urlencode_rfc3986($this->secret)."&".$this->_urlencode_rfc3986($tokenSecret);
// make signature
$sig = $this->_urlencode_rfc3986(base64_encode(hash_hmac('sha1', $baseString, $secret, TRUE)));
// BUILD URL
$url = $this->request_statuses_url; // twitter update url
$paramString = $concatenatedParams."&oauth_signature=".$sig;
// Send to cURL
$result = $this->_http($url, $paramString);
if($result['httpCode'] == '200'){
// Return array
return $result;
}
else{
// Error
show_error($result['httpCode'], $result['httpCode']);
return FALSE;
}
}
Is $status your tweet? Take a look at the POST request before you post it, my guess is _urlencode_rfc3986() converts it so that you get "$status=This+is+my+tweet" when you want "$status=This is my tweet"
Twitter is not supporting "+" as escape for spaces, which as far as I know is a violation of the standard.
You have to replace the the + with %20.
I’ve run into a limitation in the cURL bindings for PHP. It appears there is no easy way to send the same multiple values for the same key for postfields. Most of the workarounds I have come across for this have involved creating the URL encoded post fields by hand tag=foo&tag=bar&tag=baz) instead of using the associative array version of CURLOPT_POSTFIELDS.
It seems like a pretty common thing to need to support so I feel like I must have missed something. Is this really the only way to handle multiple values for the same key?
While this workaround might be considered workable (if not really annoying), my main problem is that I need to be able to do multiple values for the same key and also support file upload. As far as I can tell, file upload more or less requires to use the associate arravy version of CURLOPT_POSTFIELDS. So I feel like I am stuck.
I have posted about this problem in more detail on the cURL PHP mailing list in the hopes that someone there has some ideas about this.
Suggestions or hints on where I can look for more information on this are greatly appreciated!
I ended up writing my own function to build a custom CURLOPT_POSTFIELDS string with multipart/form-data. What a pain.
function curl_setopt_custom_postfields($ch, $postfields, $headers = null) {
// $postfields is an assoc array.
// Creates a boundary.
// Reads each postfields, detects which are #files, and which values are arrays
// and dumps them into a new array (not an assoc array) so each key can exist
// multiple times.
// Sets content-length, content-type and sets CURLOPT_POSTFIELDS with the
// generated body.
}
I was able to use this method like this:
curl_setopt_custom_postfields($ch, array(
'file' => '#/path/to/file',
'tag' => array('a', 'b', 'c'),
));
I am not certain of CURLOPT_HTTPHEADER stacks, so since this method calls it, I made certain that the function would allow for the user to specify additonal headers if needed.
I have the full code available in this blog post.
If you use tag[] rather than tag for the name, PHP will generate an array for you, in other words, rather than
tag=foo&tag=bar&tag=baz
You need
tag[]=foo&tag[]=bar&tag[]=baz
Note that when urlencoded for transmission this should become
tag%5B%5D=foo&tag%5B%5D=bar&tag%5B%5D=baz
Vote for PHP Bug #51634.
Try #BeauSimensen's answer.
Guzzle can do this. See an example below.
$client = new \GuzzleHttp\Client();
$client->request('POST', $url, [
'multipart' => [
[ 'name' => 'foo', 'contents' => 'bar' ],
[ 'name' => 'foo', 'contents' => 'baz' ],
]
]);
I ran into the same issue. But I was able to solve it this way.
for($cnt = 0; $cnt < count($siteRows); $cnt++)
{
$curlParams['site_ids['.$cnt.']'] = $siteRows[$cnt]->site_id;
}
Works for files too:
for($cnt = 0; $cnt < count($imageRows); $cnt++)
{
$curlParams['product_images['.$cnt.']'] = '#'.$imageRows[$cnt]->full_path;
}
I got it working using:
curl_setopt($ch, CURLOPT_POSTFIELDS,array('tag[0]'=>'val0','tag[1]'=>'val1'));
then $_POST results in: $_POST['tag'][0] = 'val0' and $_POST['tag'][1] = 'val1'
I think the established standard for multiple values in one key (or the same key) is to have it concatenated with a delimiter, such as for multiple selections of option lists in form elements. I believe this delimiter is the tab character (\t) or the pipe symbol (|).
If the keyname is terminated with [] (like tag[]), PHP will automatically convert the values into an array for your convenience.
lImbus and paul, thank you for your input.
If I had control over the form I am posting to, I could probably find an alternate solution to this problem. However, I do not have any control over the form. And I am almost positive that the software reading the post is not PHP and does not obey the tag[] standards.
Even if it did, cURL does not seem to obey the tag[] syntax either. Basically, I tried the following and neither worked...
curl_setopt($ch, CURLOPT_POSTFIELDS, array('file' => '#/pathtofile', 'tag[]' => array('a', 'b', 'c'));
curl_setopt($ch, CURLOPT_POSTFIELDS, array('file' => '#/pathtofile', 'tag' => array('a', 'b', 'c'));
And again, I don't think that passing tag[] would work anyway as the form I am posting to is actually looking for 'tag' and not 'tag[]'.
I am really starting to get the feeling that the cURL PHP bindings really have no support for this. Which seems so surprising to me. It seems like it can do quite literally anything else, yet it is unable to do something simple like this?
DON'T USE GUZZLE:
# at your command line start php interactive
user#group:~:php -a
php > $arr=array('var' => array(1,2,3,4));
php > echo http_build_query($arr);
var%5B0%5D=1&var%5B1%5D=2&var%5B2%5D=3&var%5B3%5D=4
php > echo urldecode(http_build_query($arr));
var[0]=1&var[1]=2&var[2]=3&var[3]=4
So, you need http_build_query where you pass a hash array of key-values; your (array) variable is entered as a key with value a array instead a scalar value like 'var' => array(1,2,3,4). Now, http_build_query can format the post fields of curl command:
$fields = array('key1' => 'value1', 'var' => array(1,2,3,4));
$curlPost = \http_build_query($fields);
curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);
that's 3 lines of code! how many 1000s of code lines are in Guzzle? (*)
So far, I used curl to:
manage Google OAuth protocol with success
connect with APIs like mailgun
handle paypal smart buttons
that's a replacement of million of lines with some 100s!
(*): the result of http_build_query can be formatted further according your needs.
I ran into the same problem in which I had to send a parameter which has to be an array from a PHP server to another server that does not use '[]' for mixing values with the same key along with a file.
In Laravel 8 I could achieve this goal with Http client (of course Http client uses guzzle).
Here is a sample of my code.
Illuminate\Support\Facades\Http::attach('file', $fileContents, 'file-name')
->post('https://destination' , [['name' => 'tag', 'content' => 'foo'], ['name' => 'tag', 'content' => 'bar']])
I found this answer online and want to post it here before it disappears:
http://yeehuichan.wordpress.com/2011/08/07/sending-multiple-values-with-the-same-namekey-in-curl-post/
function curl_setopt_custom_postfields($ch, $postfields, $headers = null) {
$algos = hash_algos();
$hashAlgo = null;
foreach ( array('sha1', 'md5') as $preferred ) {
if ( in_array($preferred, $algos) ) {
$hashAlgo = $preferred;
break;
}
}
if ( $hashAlgo === null ) { list($hashAlgo) = $algos; }
$boundary =
'----------------------------' .
substr(hash($hashAlgo, 'cURL-php-multiple-value-same-key-support' . microtime()), 0, 12);
$body = array();
$crlf = "\r\n";
$fields = array();
foreach ( $postfields as $key => $value ) {
if ( is_array($value) ) {
foreach ( $value as $v ) {
$fields[] = array($key, $v);
}
} else {
$fields[] = array($key, $value);
}
}
foreach ( $fields as $field ) {
list($key, $value) = $field;
if ( strpos($value, '#') === 0 ) {
preg_match('/^#(.*?)$/', $value, $matches);
list($dummy, $filename) = $matches;
$body[] = '--' . $boundary;
$body[] = 'Content-Disposition: form-data; name="' . $key . '"; filename="' . basename($filename) . '"';
$body[] = 'Content-Type: application/octet-stream';
$body[] = '';
$body[] = file_get_contents($filename);
} else {
$body[] = '--' . $boundary;
$body[] = 'Content-Disposition: form-data; name="' . $key . '"';
$body[] = '';
$body[] = $value;
}
}
$body[] = '--' . $boundary . '--';
$body[] = '';
$contentType = 'multipart/form-data; boundary=' . $boundary;
$content = join($crlf, $body);
$contentLength = strlen($content);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Length: ' . $contentLength,
'Expect: 100-continue',
'Content-Type: ' . $contentType,
));
curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
}
And to use it:
curl_setopt_custom_postfields($ch, array(
'file' => '#a.csv',
'name' => array('James', 'Peter', 'Richard'),
));