/includes/gateways/simplify-commerce/includes/Simplify/Http.php

  1. <?php 
  2. /** 
  3. * Copyright (c) 2013 - 2015 MasterCard International Incorporated 
  4. * All rights reserved. 
  5. * 
  6. * Redistribution and use in source and binary forms, with or without modification, are 
  7. * permitted provided that the following conditions are met: 
  8. * 
  9. * Redistributions of source code must retain the above copyright notice, this list of 
  10. * conditions and the following disclaimer. 
  11. * Redistributions in binary form must reproduce the above copyright notice, this list of 
  12. * conditions and the following disclaimer in the documentation and/or other materials 
  13. * provided with the distribution. 
  14. * Neither the name of the MasterCard International Incorporated nor the names of its 
  15. * contributors may be used to endorse or promote products derived from this software 
  16. * without specific prior written permission. 
  17. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
  18. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
  19. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
  20. * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,  
  21. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
  22. * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
  23. * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
  24. * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 
  25. * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
  26. * SUCH DAMAGE. 
  27. */ 
  28.  
  29.  
  30. class Simplify_HTTP 
  31. const DELETE = "DELETE"; 
  32. const GET = "GET"; 
  33. const POST = "POST"; 
  34. const PUT = "PUT"; 
  35.  
  36. const HTTP_SUCCESS = 200; 
  37. const HTTP_REDIRECTED = 302; 
  38. const HTTP_UNAUTHORIZED = 401; 
  39. const HTTP_NOT_FOUND = 404; 
  40. const HTTP_NOT_ALLOWED = 405; 
  41. const HTTP_BAD_REQUEST = 400; 
  42.  
  43.  
  44. const JWS_NUM_HEADERS = 7; 
  45. const JWS_ALGORITHM = 'HS256'; 
  46. const JWS_TYPE = 'JWS'; 
  47. const JWS_HDR_UNAME = 'uname'; 
  48. const JWS_HDR_URI = 'api.simplifycommerce.com/uri'; 
  49. const JWS_HDR_TIMESTAMP = 'api.simplifycommerce.com/timestamp'; 
  50. const JWS_HDR_NONCE = 'api.simplifycommerce.com/nonce'; 
  51. const JWS_HDR_TOKEN = 'api.simplifycommerce.com/token'; 
  52. const JWS_MAX_TIMESTAMP_DIFF = 300; // 5 minutes in seconds 
  53.  
  54. static private $_validMethods = array( 
  55. "post" => self::POST,  
  56. "put" => self::PUT,  
  57. "get" => self::GET,  
  58. "delete" => self::DELETE); 
  59.  
  60. private function request($url, $method, $authentication, $payload = '') 
  61. if ($authentication->publicKey == null) { 
  62. throw new InvalidArgumentException('Must have a valid public key to connect to the API'); 
  63.  
  64. if ($authentication->privateKey == null) { 
  65. throw new InvalidArgumentException('Must have a valid API key to connect to the API'); 
  66.  
  67. if (!array_key_exists(strtolower($method), self::$_validMethods)) { 
  68. throw new InvalidArgumentException('Invalid method: '.strtolower($method)); 
  69.  
  70. $method = self::$_validMethods[strtolower($method)]; 
  71.  
  72. $curl = curl_init(); 
  73.  
  74. $options = array(); 
  75.  
  76. $options[CURLOPT_URL] = $url; 
  77. $options[CURLOPT_CUSTOMREQUEST] = $method; 
  78. $options[CURLOPT_RETURNTRANSFER] = true; 
  79. $options[CURLOPT_FAILONERROR] = false; 
  80.  
  81. $signature = $this->jwsEncode($authentication, $url, $payload, $method == self::POST || $method == self::PUT); 
  82.  
  83. if ($method == self::POST || $method == self::PUT) { 
  84. $headers = array( 
  85. 'Content-type: application/json' 
  86. ); 
  87. $options[CURLOPT_POSTFIELDS] = $signature; 
  88. } else { 
  89. $headers = array( 
  90. 'Authorization: JWS ' . $signature 
  91. ); 
  92.  
  93. array_push($headers, 'Accept: application/json'); 
  94. $user_agent = 'PHP-SDK/' . Simplify_Constants::VERSION; 
  95. if (Simplify::$userAgent != null) { 
  96. $user_agent = $user_agent . ' ' . Simplify::$userAgent; 
  97. array_push($headers, 'User-Agent: ' . $user_agent); 
  98.  
  99. $options[CURLOPT_HTTPHEADER] = $headers; 
  100.  
  101. curl_setopt_array($curl, $options); 
  102.  
  103. $data = curl_exec($curl); 
  104. $errno = curl_errno($curl); 
  105. $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); 
  106.  
  107. if ($data == false || $errno != CURLE_OK) { 
  108. throw new Simplify_ApiConnectionException(curl_error($curl)); 
  109.  
  110. $object = json_decode($data, true); 
  111. //'typ' => self::JWS_TYPE,  
  112. $response = array('status' => $status, 'object' => $object); 
  113.  
  114. return $response; 
  115. curl_close($curl); 
  116.  
  117. /** 
  118. * Handles Simplify API requests 
  119. * 
  120. * @param $url 
  121. * @param $method 
  122. * @param $authentication 
  123. * @param string $payload 
  124. * @return mixed 
  125. * @throws Simplify_AuthenticationException 
  126. * @throws Simplify_ObjectNotFoundException 
  127. * @throws Simplify_BadRequestException 
  128. * @throws Simplify_NotAllowedException 
  129. * @throws Simplify_SystemException 
  130. */ 
  131. public function apiRequest($url, $method, $authentication, $payload = '') { 
  132.  
  133. $response = $this->request($url, $method, $authentication, $payload); 
  134.  
  135. $status = $response['status']; 
  136. $object = $response['object']; 
  137.  
  138. if ($status == self::HTTP_SUCCESS) { 
  139. return $object; 
  140.  
  141. if ($status == self::HTTP_REDIRECTED) { 
  142. throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object); 
  143. } else if ($status == self::HTTP_BAD_REQUEST) { 
  144. throw new Simplify_BadRequestException("Bad request", $status, $object); 
  145. } else if ($status == self::HTTP_UNAUTHORIZED) { 
  146. throw new Simplify_AuthenticationException("You are not authorized to make this request. Are you using the correct API keys?", $status, $object); 
  147. } else if ($status == self::HTTP_NOT_FOUND) { 
  148. throw new Simplify_ObjectNotFoundException("Object not found", $status, $object); 
  149. } else if ($status == self::HTTP_NOT_ALLOWED) { 
  150. throw new Simplify_NotAllowedException("Operation not allowed", $status, $object); 
  151. } else if ($status < 500) { 
  152. throw new Simplify_BadRequestException("Bad request", $status, $object); 
  153. throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object); 
  154.  
  155. /** 
  156. * Handles Simplify OAuth requests 
  157. * 
  158. * @param $url 
  159. * @param $payload 
  160. * @param $authentication 
  161. * @return mixed 
  162. * @throws Simplify_AuthenticationException 
  163. * @throws Simplify_ObjectNotFoundException 
  164. * @throws Simplify_BadRequestException 
  165. * @throws Simplify_NotAllowedException 
  166. * @throws Simplify_SystemException 
  167. */ 
  168. public function oauthRequest($url, $payload, $authentication) { 
  169.  
  170. $response = $this->request($url, Simplify_HTTP::POST, $authentication, $payload); 
  171.  
  172. $status = $response['status']; 
  173. $object = $response['object']; 
  174.  
  175. if ($status == self::HTTP_SUCCESS) { 
  176. return $object; 
  177.  
  178. $error = $object['error']; 
  179. $error_description = $object['error_description']; 
  180.  
  181. if ($status == self::HTTP_REDIRECTED) { 
  182. throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object); 
  183. } else if ($status == self::HTTP_BAD_REQUEST) { 
  184.  
  185. if ( $error == 'invalid_request') { 
  186. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Error during OAuth request', $error, $error_description)); 
  187. }else if ($error == 'unsupported_grant_type') { 
  188. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unsupported grant type in OAuth request', $error, $error_description)); 
  189. }else if ($error == 'invalid_scope') { 
  190. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Invalid scope in OAuth request', $error, $error_description)); 
  191. }else{ 
  192. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unknown OAuth error', $error, $error_description)); 
  193.  
  194. //TODO: build BadRequestException error JSON 
  195.  
  196. } else if ($status == self::HTTP_UNAUTHORIZED) { 
  197.  
  198. if ($error == 'access_denied') { 
  199. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Access denied for OAuth request', $error, $error_description)); 
  200. }else if ($error == 'invalid_client') { 
  201. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Invalid client ID in OAuth request', $error, $error_description)); 
  202. }else if ($error == 'unauthorized_client') { 
  203. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unauthorized client in OAuth request', $error, $error_description)); 
  204. }else{ 
  205. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unknown authentication error', $error, $error_description)); 
  206.  
  207. } else if ($status < 500) { 
  208. throw new Simplify_BadRequestException("Bad request", $status, $object); 
  209. throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object); 
  210.  
  211. public function jwsDecode($authentication, $hash) 
  212. if ($authentication->publicKey == null) { 
  213. throw new InvalidArgumentException('Must have a valid public key to connect to the API'); 
  214.  
  215. if ($authentication->privateKey == null) { 
  216. throw new InvalidArgumentException('Must have a valid API key to connect to the API'); 
  217.  
  218. if (!isset($hash['payload'])) { 
  219. throw new InvalidArgumentException('Event data is Missing payload'); 
  220. $payload = trim($hash['payload']); 
  221.  
  222.  
  223. try { 
  224. $parts = explode('.', $payload); 
  225. if (count($parts) != 3) { 
  226. $this->jwsAuthError("Incorrectly formatted JWS message"); 
  227.  
  228. $headerStr = $this->jwsUrlSafeDecode64($parts[0]); 
  229. $bodyStr = $this->jwsUrlSafeDecode64($parts[1]); 
  230. $sigStr = $parts[2]; 
  231.  
  232. $url = null; 
  233. if (isset($hash['url'])) { 
  234. $url = $hash['url']; 
  235. $this->jwsVerifyHeader($headerStr, $url, $authentication->publicKey); 
  236.  
  237. $msg = $parts[0] . "." . $parts[1]; 
  238. if (!$this->jwsVerifySignature($authentication->privateKey, $msg, $sigStr)) { 
  239. $this->jwsAuthError("JWS signature does not match"); 
  240.  
  241. return $bodyStr; 
  242.  
  243. } catch (ApiException $e) { 
  244. throw $e; 
  245. } catch (Exception $e) { 
  246. $this->jwsAuthError("Exception during JWS decoding: " . $e); 
  247.  
  248. private function jwsEncode($authentication, $url, $payload, $hasPayload) 
  249. // TODO - better seeding of RNG 
  250. $jws_hdr = array('typ' => self::JWS_TYPE,  
  251. 'alg' => self::JWS_ALGORITHM,  
  252. 'kid' => $authentication->publicKey,  
  253. self::JWS_HDR_URI => $url,  
  254. self::JWS_HDR_TIMESTAMP => sprintf("%u000", round(microtime(true))),  
  255. self::JWS_HDR_NONCE => sprintf("%u", mt_rand()),  
  256. ); 
  257.  
  258. // add oauth token if provided 
  259. if ( !empty($authentication->accessToken) ) { 
  260. $jws_hdr[self::JWS_HDR_TOKEN] = $authentication->accessToken; 
  261.  
  262. $header = $this->jwsUrlSafeEncode64(json_encode($jws_hdr)); 
  263.  
  264. if ($hasPayload) { 
  265. $payload = $this->jwsUrlSafeEncode64($payload); 
  266. } else { 
  267. $payload = ''; 
  268.  
  269. $msg = $header . "." . $payload; 
  270. return $msg . "." . $this->jwsSign($authentication->privateKey, $msg); 
  271.  
  272. private function jwsSign($privateKey, $msg) { 
  273. $decodedPrivateKey = $this->jwsUrlSafeDecode64($privateKey); 
  274. $sig = hash_hmac('sha256', $msg, $decodedPrivateKey, true); 
  275.  
  276. return $this->jwsUrlSafeEncode64($sig); 
  277.  
  278. private function jwsVerifyHeader($header, $url, $publicKey) { 
  279.  
  280. $hdr = json_decode($header, true); 
  281.  
  282. if (count($hdr) != self::JWS_NUM_HEADERS) { 
  283. $this->jwsAuthError("Incorrect number of JWS header parameters - found " . count($hdr) . " required " . self::JWS_NUM_HEADERS); 
  284.  
  285. if ($hdr['alg'] != self::JWS_ALGORITHM) { 
  286. $this->jwsAuthError("Incorrect algorithm - found " . $hdr['alg'] . " required " . self::WS_ALGORITHM); 
  287.  
  288. if ($hdr['typ'] != self::JWS_TYPE) { 
  289. $this->jwsAuthError("Incorrect type - found " . $hdr['typ'] . " required " . self::JWS_TYPE); 
  290.  
  291. if ($hdr['kid'] == null) { 
  292. $this->jwsAuthError("Missing Key ID"); 
  293.  
  294. if ($hdr['kid'] != $publicKey) { 
  295. if ($this->isLiveKey($publicKey)) { 
  296. $this->jwsAuthError("Invalid Key ID"); 
  297.  
  298. if ($hdr[self::JWS_HDR_URI] == null) { 
  299. $this->jwsAuthError("Missing URI"); 
  300.  
  301. if ($url != null && $hdr[self::JWS_HDR_URI] != $url) { 
  302. $this->jwsAuthError("Incorrect URL - found " . $hdr[self::JWS_HDR_URI] . " required " . $url); 
  303.  
  304.  
  305. if ($hdr[self::JWS_HDR_TIMESTAMP] == null) { 
  306. $this->jwsAuthError("Missing timestamp"); 
  307.  
  308. if (!$this->jwsVerifyTimestamp($hdr[self::JWS_HDR_TIMESTAMP])) { 
  309. $this->jwsAuthError("Invalid timestamp"); 
  310.  
  311. if ($hdr[self::JWS_HDR_NONCE] == null) { 
  312. $this->jwsAuthError("Missing nonce"); 
  313.  
  314. if ($hdr[self::JWS_HDR_UNAME] == null) { 
  315. $this->jwsAuthError("Missing username"); 
  316.  
  317.  
  318. private function jwsVerifySignature($privateKey, $msg, $expectedSig) { 
  319. return $this->jwsSign($privateKey, $msg) == $expectedSig; 
  320.  
  321. private function jwsAuthError($reason) { 
  322. throw new Simplify_AuthenticationException("JWS authentication failure: " . $reason); 
  323.  
  324. private function jwsVerifyTimestamp($ts) { 
  325. $now = round(microtime(true)); // Seconds 
  326. return abs($now - $ts / 1000) < self::JWS_MAX_TIMESTAMP_DIFF; 
  327.  
  328. private function isLiveKey($k) { 
  329. return strpos($k, "lvpb") === 0; 
  330.  
  331. private function jwsUrlSafeEncode64($s) { 
  332. return str_replace(array('+', '/', '='),  
  333. array('-', '_', ''),  
  334. base64_encode($s)); 
  335.  
  336. private function jwsUrlSafeDecode64($s) { 
  337.  
  338. switch (strlen($s) % 4) { 
  339. case 0: break; 
  340. case 2: $s = $s . "=="; 
  341. break; 
  342. case 3: $s = $s . "="; 
  343. break; 
  344. default: throw new InvalidArgumentException('incorrecly formatted JWS payload'); 
  345. return base64_decode(str_replace(array('-', '_'), array('+', '/'), $s)); 
  346.  
  347. private function buildOauthError($msg, $error, $error_description) { 
  348.  
  349. return array( 
  350. 'error' => array( 
  351. 'code' => 'oauth_error',  
  352. 'message' => $msg.', error code: '.$error.', description: '.$error_description.'' 
  353. ); 
.