woocommerce/includes/gateways/simplify-commerce/includes/Simplify/Http.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.''
)
);
}
}