408 lines
14 KiB
PHP
408 lines
14 KiB
PHP
<?php
|
|
/*
|
|
* Copyright (c) 2013 - 2015 MasterCard International Incorporated
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without modification, are
|
|
* permitted provided that the following conditions are met:
|
|
*
|
|
* Redistributions of source code must retain the above copyright notice, this list of
|
|
* conditions and the following disclaimer.
|
|
* Redistributions in binary form must reproduce the above copyright notice, this list of
|
|
* conditions and the following disclaimer in the documentation and/or other materials
|
|
* provided with the distribution.
|
|
* Neither the name of the MasterCard International Incorporated nor the names of its
|
|
* contributors may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
|
|
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
|
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
|
|
class Simplify_HTTP
|
|
{
|
|
const DELETE = "DELETE";
|
|
const GET = "GET";
|
|
const POST = "POST";
|
|
const PUT = "PUT";
|
|
|
|
const HTTP_SUCCESS = 200;
|
|
const HTTP_REDIRECTED = 302;
|
|
const HTTP_UNAUTHORIZED = 401;
|
|
const HTTP_NOT_FOUND = 404;
|
|
const HTTP_NOT_ALLOWED = 405;
|
|
const HTTP_BAD_REQUEST = 400;
|
|
|
|
|
|
const JWS_NUM_HEADERS = 7;
|
|
const JWS_ALGORITHM = 'HS256';
|
|
const JWS_TYPE = 'JWS';
|
|
const JWS_HDR_UNAME = 'uname';
|
|
const JWS_HDR_URI = 'api.simplifycommerce.com/uri';
|
|
const JWS_HDR_TIMESTAMP = 'api.simplifycommerce.com/timestamp';
|
|
const JWS_HDR_NONCE = 'api.simplifycommerce.com/nonce';
|
|
const JWS_HDR_TOKEN = 'api.simplifycommerce.com/token';
|
|
const JWS_MAX_TIMESTAMP_DIFF = 300; // 5 minutes in seconds
|
|
|
|
static private $_validMethods = array(
|
|
"post" => self::POST,
|
|
"put" => self::PUT,
|
|
"get" => self::GET,
|
|
"delete" => self::DELETE);
|
|
|
|
private function request($url, $method, $authentication, $payload = '')
|
|
{
|
|
if ($authentication->publicKey == null) {
|
|
throw new InvalidArgumentException('Must have a valid public key to connect to the API');
|
|
}
|
|
|
|
if ($authentication->privateKey == null) {
|
|
throw new InvalidArgumentException('Must have a valid API key to connect to the API');
|
|
}
|
|
|
|
if (!array_key_exists(strtolower($method), self::$_validMethods)) {
|
|
throw new InvalidArgumentException('Invalid method: '.strtolower($method));
|
|
}
|
|
|
|
$method = self::$_validMethods[strtolower($method)];
|
|
|
|
$curl = curl_init();
|
|
|
|
$options = array();
|
|
|
|
$options[CURLOPT_URL] = $url;
|
|
$options[CURLOPT_CUSTOMREQUEST] = $method;
|
|
$options[CURLOPT_RETURNTRANSFER] = true;
|
|
$options[CURLOPT_FAILONERROR] = false;
|
|
|
|
$signature = $this->jwsEncode($authentication, $url, $payload, $method == self::POST || $method == self::PUT);
|
|
|
|
if ($method == self::POST || $method == self::PUT) {
|
|
$headers = array(
|
|
'Content-type: application/json'
|
|
);
|
|
$options[CURLOPT_POSTFIELDS] = $signature;
|
|
} else {
|
|
$headers = array(
|
|
'Authorization: JWS ' . $signature
|
|
);
|
|
}
|
|
|
|
array_push($headers, 'Accept: application/json');
|
|
$user_agent = 'PHP-SDK/' . Simplify_Constants::VERSION;
|
|
if (Simplify::$userAgent != null) {
|
|
$user_agent = $user_agent . ' ' . Simplify::$userAgent;
|
|
}
|
|
array_push($headers, 'User-Agent: ' . $user_agent);
|
|
|
|
$options[CURLOPT_HTTPHEADER] = $headers;
|
|
|
|
curl_setopt_array($curl, $options);
|
|
|
|
$data = curl_exec($curl);
|
|
$errno = curl_errno($curl);
|
|
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
|
|
if ($data == false || $errno != CURLE_OK) {
|
|
throw new Simplify_ApiConnectionException(curl_error($curl));
|
|
}
|
|
|
|
$object = json_decode($data, true);
|
|
//'typ' => self::JWS_TYPE,
|
|
$response = array('status' => $status, 'object' => $object);
|
|
|
|
return $response;
|
|
curl_close($curl);
|
|
}
|
|
|
|
/**
|
|
* Handles Simplify API requests
|
|
*
|
|
* @param $url
|
|
* @param $method
|
|
* @param $authentication
|
|
* @param string $payload
|
|
* @return mixed
|
|
* @throws Simplify_AuthenticationException
|
|
* @throws Simplify_ObjectNotFoundException
|
|
* @throws Simplify_BadRequestException
|
|
* @throws Simplify_NotAllowedException
|
|
* @throws Simplify_SystemException
|
|
*/
|
|
public function apiRequest($url, $method, $authentication, $payload = ''){
|
|
|
|
$response = $this->request($url, $method, $authentication, $payload);
|
|
|
|
$status = $response['status'];
|
|
$object = $response['object'];
|
|
|
|
if ($status == self::HTTP_SUCCESS) {
|
|
return $object;
|
|
}
|
|
|
|
if ($status == self::HTTP_REDIRECTED) {
|
|
throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object);
|
|
} else if ($status == self::HTTP_BAD_REQUEST) {
|
|
throw new Simplify_BadRequestException("Bad request", $status, $object);
|
|
} else if ($status == self::HTTP_UNAUTHORIZED) {
|
|
throw new Simplify_AuthenticationException("You are not authorized to make this request. Are you using the correct API keys?", $status, $object);
|
|
} else if ($status == self::HTTP_NOT_FOUND) {
|
|
throw new Simplify_ObjectNotFoundException("Object not found", $status, $object);
|
|
} else if ($status == self::HTTP_NOT_ALLOWED) {
|
|
throw new Simplify_NotAllowedException("Operation not allowed", $status, $object);
|
|
} else if ($status < 500) {
|
|
throw new Simplify_BadRequestException("Bad request", $status, $object);
|
|
}
|
|
throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object);
|
|
}
|
|
|
|
/**
|
|
* Handles Simplify OAuth requests
|
|
*
|
|
* @param $url
|
|
* @param $payload
|
|
* @param $authentication
|
|
* @return mixed
|
|
* @throws Simplify_AuthenticationException
|
|
* @throws Simplify_ObjectNotFoundException
|
|
* @throws Simplify_BadRequestException
|
|
* @throws Simplify_NotAllowedException
|
|
* @throws Simplify_SystemException
|
|
*/
|
|
public function oauthRequest($url, $payload, $authentication){
|
|
|
|
$response = $this->request($url, Simplify_HTTP::POST, $authentication, $payload);
|
|
|
|
$status = $response['status'];
|
|
$object = $response['object'];
|
|
|
|
if ($status == self::HTTP_SUCCESS) {
|
|
return $object;
|
|
}
|
|
|
|
$error = $object['error'];
|
|
$error_description = $object['error_description'];
|
|
|
|
if ($status == self::HTTP_REDIRECTED) {
|
|
throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object);
|
|
} else if ($status == self::HTTP_BAD_REQUEST) {
|
|
|
|
if ( $error == 'invalid_request'){
|
|
throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Error during OAuth request', $error, $error_description));
|
|
}else if ($error == 'unsupported_grant_type'){
|
|
throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unsupported grant type in OAuth request', $error, $error_description));
|
|
}else if ($error == 'invalid_scope'){
|
|
throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Invalid scope in OAuth request', $error, $error_description));
|
|
}else{
|
|
throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unknown OAuth error', $error, $error_description));
|
|
}
|
|
|
|
//TODO: build BadRequestException error JSON
|
|
|
|
} else if ($status == self::HTTP_UNAUTHORIZED){
|
|
|
|
if ($error == 'access_denied'){
|
|
throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Access denied for OAuth request', $error, $error_description));
|
|
}else if ($error == 'invalid_client'){
|
|
throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Invalid client ID in OAuth request', $error, $error_description));
|
|
}else if ($error == 'unauthorized_client'){
|
|
throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unauthorized client in OAuth request', $error, $error_description));
|
|
}else{
|
|
throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unknown authentication error', $error, $error_description));
|
|
}
|
|
|
|
} else if ($status < 500) {
|
|
throw new Simplify_BadRequestException("Bad request", $status, $object);
|
|
}
|
|
throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object);
|
|
}
|
|
|
|
public function jwsDecode($authentication, $hash)
|
|
{
|
|
if ($authentication->publicKey == null) {
|
|
throw new InvalidArgumentException('Must have a valid public key to connect to the API');
|
|
}
|
|
|
|
if ($authentication->privateKey == null) {
|
|
throw new InvalidArgumentException('Must have a valid API key to connect to the API');
|
|
}
|
|
|
|
if (!isset($hash['payload'])) {
|
|
throw new InvalidArgumentException('Event data is Missing payload');
|
|
}
|
|
$payload = trim($hash['payload']);
|
|
|
|
|
|
try {
|
|
$parts = explode('.', $payload);
|
|
if (count($parts) != 3) {
|
|
$this->jwsAuthError("Incorrectly formatted JWS message");
|
|
}
|
|
|
|
$headerStr = $this->jwsUrlSafeDecode64($parts[0]);
|
|
$bodyStr = $this->jwsUrlSafeDecode64($parts[1]);
|
|
$sigStr = $parts[2];
|
|
|
|
$url = null;
|
|
if (isset($hash['url'])) {
|
|
$url = $hash['url'];
|
|
}
|
|
$this->jwsVerifyHeader($headerStr, $url, $authentication->publicKey);
|
|
|
|
$msg = $parts[0] . "." . $parts[1];
|
|
if (!$this->jwsVerifySignature($authentication->privateKey, $msg, $sigStr)) {
|
|
$this->jwsAuthError("JWS signature does not match");
|
|
}
|
|
|
|
return $bodyStr;
|
|
|
|
} catch (ApiException $e) {
|
|
throw $e;
|
|
} catch (Exception $e) {
|
|
$this->jwsAuthError("Exception during JWS decoding: " . $e);
|
|
}
|
|
}
|
|
|
|
private function jwsEncode($authentication, $url, $payload, $hasPayload)
|
|
{
|
|
// TODO - better seeding of RNG
|
|
$jws_hdr = array('typ' => self::JWS_TYPE,
|
|
'alg' => self::JWS_ALGORITHM,
|
|
'kid' => $authentication->publicKey,
|
|
self::JWS_HDR_URI => $url,
|
|
self::JWS_HDR_TIMESTAMP => sprintf("%u000", round(microtime(true))),
|
|
self::JWS_HDR_NONCE => sprintf("%u", mt_rand()),
|
|
);
|
|
|
|
// add oauth token if provided
|
|
if ( !empty($authentication->accessToken) ){
|
|
$jws_hdr[self::JWS_HDR_TOKEN] = $authentication->accessToken;
|
|
}
|
|
|
|
$header = $this->jwsUrlSafeEncode64(json_encode($jws_hdr));
|
|
|
|
if ($hasPayload) {
|
|
$payload = $this->jwsUrlSafeEncode64($payload);
|
|
} else {
|
|
$payload = '';
|
|
}
|
|
|
|
$msg = $header . "." . $payload;
|
|
return $msg . "." . $this->jwsSign($authentication->privateKey, $msg);
|
|
}
|
|
|
|
private function jwsSign($privateKey, $msg) {
|
|
$decodedPrivateKey = $this->jwsUrlSafeDecode64($privateKey);
|
|
$sig = hash_hmac('sha256', $msg, $decodedPrivateKey, true);
|
|
|
|
return $this->jwsUrlSafeEncode64($sig);
|
|
}
|
|
|
|
private function jwsVerifyHeader($header, $url, $publicKey) {
|
|
|
|
$hdr = json_decode($header, true);
|
|
|
|
if (count($hdr) != self::JWS_NUM_HEADERS) {
|
|
$this->jwsAuthError("Incorrect number of JWS header parameters - found " . count($hdr) . " required " . self::JWS_NUM_HEADERS);
|
|
}
|
|
|
|
if ($hdr['alg'] != self::JWS_ALGORITHM) {
|
|
$this->jwsAuthError("Incorrect algorithm - found " . $hdr['alg'] . " required " . self::WS_ALGORITHM);
|
|
}
|
|
|
|
if ($hdr['typ'] != self::JWS_TYPE) {
|
|
$this->jwsAuthError("Incorrect type - found " . $hdr['typ'] . " required " . self::JWS_TYPE);
|
|
}
|
|
|
|
if ($hdr['kid'] == null) {
|
|
$this->jwsAuthError("Missing Key ID");
|
|
}
|
|
|
|
if ($hdr['kid'] != $publicKey) {
|
|
if ($this->isLiveKey($publicKey)) {
|
|
$this->jwsAuthError("Invalid Key ID");
|
|
}
|
|
}
|
|
|
|
if ($hdr[self::JWS_HDR_URI] == null) {
|
|
$this->jwsAuthError("Missing URI");
|
|
}
|
|
|
|
if ($url != null && $hdr[self::JWS_HDR_URI] != $url) {
|
|
$this->jwsAuthError("Incorrect URL - found " . $hdr[self::JWS_HDR_URI] . " required " . $url);
|
|
}
|
|
|
|
|
|
if ($hdr[self::JWS_HDR_TIMESTAMP] == null) {
|
|
$this->jwsAuthError("Missing timestamp");
|
|
}
|
|
|
|
if (!$this->jwsVerifyTimestamp($hdr[self::JWS_HDR_TIMESTAMP])) {
|
|
$this->jwsAuthError("Invalid timestamp");
|
|
}
|
|
|
|
if ($hdr[self::JWS_HDR_NONCE] == null) {
|
|
$this->jwsAuthError("Missing nonce");
|
|
}
|
|
|
|
if ($hdr[self::JWS_HDR_UNAME] == null) {
|
|
$this->jwsAuthError("Missing username");
|
|
}
|
|
}
|
|
|
|
|
|
private function jwsVerifySignature($privateKey, $msg, $expectedSig) {
|
|
return $this->jwsSign($privateKey, $msg) == $expectedSig;
|
|
}
|
|
|
|
private function jwsAuthError($reason) {
|
|
throw new Simplify_AuthenticationException("JWS authentication failure: " . $reason);
|
|
}
|
|
|
|
private function jwsVerifyTimestamp($ts) {
|
|
$now = round(microtime(true)); // Seconds
|
|
return abs($now - $ts / 1000) < self::JWS_MAX_TIMESTAMP_DIFF;
|
|
}
|
|
|
|
private function isLiveKey($k) {
|
|
return strpos($k, "lvpb") === 0;
|
|
}
|
|
|
|
private function jwsUrlSafeEncode64($s) {
|
|
return str_replace(array('+', '/', '='),
|
|
array('-', '_', ''),
|
|
base64_encode($s));
|
|
}
|
|
|
|
private function jwsUrlSafeDecode64($s) {
|
|
|
|
switch (strlen($s) % 4) {
|
|
case 0: break;
|
|
case 2: $s = $s . "==";
|
|
break;
|
|
case 3: $s = $s . "=";
|
|
break;
|
|
default: throw new InvalidArgumentException('incorrecly formatted JWS payload');
|
|
}
|
|
return base64_decode(str_replace(array('-', '_'), array('+', '/'), $s));
|
|
}
|
|
|
|
private function buildOauthError($msg, $error, $error_description){
|
|
|
|
return array(
|
|
'error' => array(
|
|
'code' => 'oauth_error',
|
|
'message' => $msg.', error code: '.$error.', description: '.$error_description.''
|
|
)
|
|
);
|
|
}
|
|
}
|