Simplify_HTTP

Copyright (c) 2013 - 2015 MasterCard International Incorporated All rights reserved.

Defined (1)

The class is defined in the following location(s).

/includes/gateways/simplify-commerce/includes/Simplify/Http.php  
  1. class Simplify_HTTP 
  2. const DELETE = "DELETE"; 
  3. const GET = "GET"; 
  4. const POST = "POST"; 
  5. const PUT = "PUT"; 
  6.  
  7. const HTTP_SUCCESS = 200; 
  8. const HTTP_REDIRECTED = 302; 
  9. const HTTP_UNAUTHORIZED = 401; 
  10. const HTTP_NOT_FOUND = 404; 
  11. const HTTP_NOT_ALLOWED = 405; 
  12. const HTTP_BAD_REQUEST = 400; 
  13.  
  14.  
  15. const JWS_NUM_HEADERS = 7; 
  16. const JWS_ALGORITHM = 'HS256'; 
  17. const JWS_TYPE = 'JWS'; 
  18. const JWS_HDR_UNAME = 'uname'; 
  19. const JWS_HDR_URI = 'api.simplifycommerce.com/uri'; 
  20. const JWS_HDR_TIMESTAMP = 'api.simplifycommerce.com/timestamp'; 
  21. const JWS_HDR_NONCE = 'api.simplifycommerce.com/nonce'; 
  22. const JWS_HDR_TOKEN = 'api.simplifycommerce.com/token'; 
  23. const JWS_MAX_TIMESTAMP_DIFF = 300; // 5 minutes in seconds 
  24.  
  25. static private $_validMethods = array( 
  26. "post" => self::POST,  
  27. "put" => self::PUT,  
  28. "get" => self::GET,  
  29. "delete" => self::DELETE); 
  30.  
  31. private function request($url, $method, $authentication, $payload = '') 
  32. if ($authentication->publicKey == null) { 
  33. throw new InvalidArgumentException('Must have a valid public key to connect to the API'); 
  34.  
  35. if ($authentication->privateKey == null) { 
  36. throw new InvalidArgumentException('Must have a valid API key to connect to the API'); 
  37.  
  38. if (!array_key_exists(strtolower($method), self::$_validMethods)) { 
  39. throw new InvalidArgumentException('Invalid method: '.strtolower($method)); 
  40.  
  41. $method = self::$_validMethods[strtolower($method)]; 
  42.  
  43. $curl = curl_init(); 
  44.  
  45. $options = array(); 
  46.  
  47. $options[CURLOPT_URL] = $url; 
  48. $options[CURLOPT_CUSTOMREQUEST] = $method; 
  49. $options[CURLOPT_RETURNTRANSFER] = true; 
  50. $options[CURLOPT_FAILONERROR] = false; 
  51.  
  52. $signature = $this->jwsEncode($authentication, $url, $payload, $method == self::POST || $method == self::PUT); 
  53.  
  54. if ($method == self::POST || $method == self::PUT) { 
  55. $headers = array( 
  56. 'Content-type: application/json' 
  57. ); 
  58. $options[CURLOPT_POSTFIELDS] = $signature; 
  59. } else { 
  60. $headers = array( 
  61. 'Authorization: JWS ' . $signature 
  62. ); 
  63.  
  64. array_push($headers, 'Accept: application/json'); 
  65. $user_agent = 'PHP-SDK/' . Simplify_Constants::VERSION; 
  66. if (Simplify::$userAgent != null) { 
  67. $user_agent = $user_agent . ' ' . Simplify::$userAgent; 
  68. array_push($headers, 'User-Agent: ' . $user_agent); 
  69.  
  70. $options[CURLOPT_HTTPHEADER] = $headers; 
  71.  
  72. curl_setopt_array($curl, $options); 
  73.  
  74. $data = curl_exec($curl); 
  75. $errno = curl_errno($curl); 
  76. $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); 
  77.  
  78. if ($data == false || $errno != CURLE_OK) { 
  79. throw new Simplify_ApiConnectionException(curl_error($curl)); 
  80.  
  81. $object = json_decode($data, true); 
  82. //'typ' => self::JWS_TYPE,  
  83. $response = array('status' => $status, 'object' => $object); 
  84.  
  85. return $response; 
  86. curl_close($curl); 
  87.  
  88. /** 
  89. * Handles Simplify API requests 
  90. * @param $url 
  91. * @param $method 
  92. * @param $authentication 
  93. * @param string $payload 
  94. * @return mixed 
  95. * @throws Simplify_AuthenticationException 
  96. * @throws Simplify_ObjectNotFoundException 
  97. * @throws Simplify_BadRequestException 
  98. * @throws Simplify_NotAllowedException 
  99. * @throws Simplify_SystemException 
  100. */ 
  101. public function apiRequest($url, $method, $authentication, $payload = '') { 
  102.  
  103. $response = $this->request($url, $method, $authentication, $payload); 
  104.  
  105. $status = $response['status']; 
  106. $object = $response['object']; 
  107.  
  108. if ($status == self::HTTP_SUCCESS) { 
  109. return $object; 
  110.  
  111. if ($status == self::HTTP_REDIRECTED) { 
  112. throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object); 
  113. } else if ($status == self::HTTP_BAD_REQUEST) { 
  114. throw new Simplify_BadRequestException("Bad request", $status, $object); 
  115. } else if ($status == self::HTTP_UNAUTHORIZED) { 
  116. throw new Simplify_AuthenticationException("You are not authorized to make this request. Are you using the correct API keys?", $status, $object); 
  117. } else if ($status == self::HTTP_NOT_FOUND) { 
  118. throw new Simplify_ObjectNotFoundException("Object not found", $status, $object); 
  119. } else if ($status == self::HTTP_NOT_ALLOWED) { 
  120. throw new Simplify_NotAllowedException("Operation not allowed", $status, $object); 
  121. } else if ($status < 500) { 
  122. throw new Simplify_BadRequestException("Bad request", $status, $object); 
  123. throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object); 
  124.  
  125. /** 
  126. * Handles Simplify OAuth requests 
  127. * @param $url 
  128. * @param $payload 
  129. * @param $authentication 
  130. * @return mixed 
  131. * @throws Simplify_AuthenticationException 
  132. * @throws Simplify_ObjectNotFoundException 
  133. * @throws Simplify_BadRequestException 
  134. * @throws Simplify_NotAllowedException 
  135. * @throws Simplify_SystemException 
  136. */ 
  137. public function oauthRequest($url, $payload, $authentication) { 
  138.  
  139. $response = $this->request($url, Simplify_HTTP::POST, $authentication, $payload); 
  140.  
  141. $status = $response['status']; 
  142. $object = $response['object']; 
  143.  
  144. if ($status == self::HTTP_SUCCESS) { 
  145. return $object; 
  146.  
  147. $error = $object['error']; 
  148. $error_description = $object['error_description']; 
  149.  
  150. if ($status == self::HTTP_REDIRECTED) { 
  151. throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object); 
  152. } else if ($status == self::HTTP_BAD_REQUEST) { 
  153.  
  154. if ( $error == 'invalid_request') { 
  155. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Error during OAuth request', $error, $error_description)); 
  156. }else if ($error == 'unsupported_grant_type') { 
  157. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unsupported grant type in OAuth request', $error, $error_description)); 
  158. }else if ($error == 'invalid_scope') { 
  159. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Invalid scope in OAuth request', $error, $error_description)); 
  160. }else{ 
  161. throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unknown OAuth error', $error, $error_description)); 
  162.  
  163. //TODO: build BadRequestException error JSON 
  164.  
  165. } else if ($status == self::HTTP_UNAUTHORIZED) { 
  166.  
  167. if ($error == 'access_denied') { 
  168. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Access denied for OAuth request', $error, $error_description)); 
  169. }else if ($error == 'invalid_client') { 
  170. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Invalid client ID in OAuth request', $error, $error_description)); 
  171. }else if ($error == 'unauthorized_client') { 
  172. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unauthorized client in OAuth request', $error, $error_description)); 
  173. }else{ 
  174. throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unknown authentication error', $error, $error_description)); 
  175.  
  176. } else if ($status < 500) { 
  177. throw new Simplify_BadRequestException("Bad request", $status, $object); 
  178. throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object); 
  179.  
  180. public function jwsDecode($authentication, $hash) 
  181. if ($authentication->publicKey == null) { 
  182. throw new InvalidArgumentException('Must have a valid public key to connect to the API'); 
  183.  
  184. if ($authentication->privateKey == null) { 
  185. throw new InvalidArgumentException('Must have a valid API key to connect to the API'); 
  186.  
  187. if (!isset($hash['payload'])) { 
  188. throw new InvalidArgumentException('Event data is Missing payload'); 
  189. $payload = trim($hash['payload']); 
  190.  
  191.  
  192. try { 
  193. $parts = explode('.', $payload); 
  194. if (count($parts) != 3) { 
  195. $this->jwsAuthError("Incorrectly formatted JWS message"); 
  196.  
  197. $headerStr = $this->jwsUrlSafeDecode64($parts[0]); 
  198. $bodyStr = $this->jwsUrlSafeDecode64($parts[1]); 
  199. $sigStr = $parts[2]; 
  200.  
  201. $url = null; 
  202. if (isset($hash['url'])) { 
  203. $url = $hash['url']; 
  204. $this->jwsVerifyHeader($headerStr, $url, $authentication->publicKey); 
  205.  
  206. $msg = $parts[0] . "." . $parts[1]; 
  207. if (!$this->jwsVerifySignature($authentication->privateKey, $msg, $sigStr)) { 
  208. $this->jwsAuthError("JWS signature does not match"); 
  209.  
  210. return $bodyStr; 
  211.  
  212. } catch (ApiException $e) { 
  213. throw $e; 
  214. } catch (Exception $e) { 
  215. $this->jwsAuthError("Exception during JWS decoding: " . $e); 
  216.  
  217. private function jwsEncode($authentication, $url, $payload, $hasPayload) 
  218. // TODO - better seeding of RNG 
  219. $jws_hdr = array('typ' => self::JWS_TYPE,  
  220. 'alg' => self::JWS_ALGORITHM,  
  221. 'kid' => $authentication->publicKey,  
  222. self::JWS_HDR_URI => $url,  
  223. self::JWS_HDR_TIMESTAMP => sprintf("%u000", round(microtime(true))),  
  224. self::JWS_HDR_NONCE => sprintf("%u", mt_rand()),  
  225. ); 
  226.  
  227. // add oauth token if provided 
  228. if ( !empty($authentication->accessToken) ) { 
  229. $jws_hdr[self::JWS_HDR_TOKEN] = $authentication->accessToken; 
  230.  
  231. $header = $this->jwsUrlSafeEncode64(json_encode($jws_hdr)); 
  232.  
  233. if ($hasPayload) { 
  234. $payload = $this->jwsUrlSafeEncode64($payload); 
  235. } else { 
  236. $payload = ''; 
  237.  
  238. $msg = $header . "." . $payload; 
  239. return $msg . "." . $this->jwsSign($authentication->privateKey, $msg); 
  240.  
  241. private function jwsSign($privateKey, $msg) { 
  242. $decodedPrivateKey = $this->jwsUrlSafeDecode64($privateKey); 
  243. $sig = hash_hmac('sha256', $msg, $decodedPrivateKey, true); 
  244.  
  245. return $this->jwsUrlSafeEncode64($sig); 
  246.  
  247. private function jwsVerifyHeader($header, $url, $publicKey) { 
  248.  
  249. $hdr = json_decode($header, true); 
  250.  
  251. if (count($hdr) != self::JWS_NUM_HEADERS) { 
  252. $this->jwsAuthError("Incorrect number of JWS header parameters - found " . count($hdr) . " required " . self::JWS_NUM_HEADERS); 
  253.  
  254. if ($hdr['alg'] != self::JWS_ALGORITHM) { 
  255. $this->jwsAuthError("Incorrect algorithm - found " . $hdr['alg'] . " required " . self::WS_ALGORITHM); 
  256.  
  257. if ($hdr['typ'] != self::JWS_TYPE) { 
  258. $this->jwsAuthError("Incorrect type - found " . $hdr['typ'] . " required " . self::JWS_TYPE); 
  259.  
  260. if ($hdr['kid'] == null) { 
  261. $this->jwsAuthError("Missing Key ID"); 
  262.  
  263. if ($hdr['kid'] != $publicKey) { 
  264. if ($this->isLiveKey($publicKey)) { 
  265. $this->jwsAuthError("Invalid Key ID"); 
  266.  
  267. if ($hdr[self::JWS_HDR_URI] == null) { 
  268. $this->jwsAuthError("Missing URI"); 
  269.  
  270. if ($url != null && $hdr[self::JWS_HDR_URI] != $url) { 
  271. $this->jwsAuthError("Incorrect URL - found " . $hdr[self::JWS_HDR_URI] . " required " . $url); 
  272.  
  273.  
  274. if ($hdr[self::JWS_HDR_TIMESTAMP] == null) { 
  275. $this->jwsAuthError("Missing timestamp"); 
  276.  
  277. if (!$this->jwsVerifyTimestamp($hdr[self::JWS_HDR_TIMESTAMP])) { 
  278. $this->jwsAuthError("Invalid timestamp"); 
  279.  
  280. if ($hdr[self::JWS_HDR_NONCE] == null) { 
  281. $this->jwsAuthError("Missing nonce"); 
  282.  
  283. if ($hdr[self::JWS_HDR_UNAME] == null) { 
  284. $this->jwsAuthError("Missing username"); 
  285.  
  286.  
  287. private function jwsVerifySignature($privateKey, $msg, $expectedSig) { 
  288. return $this->jwsSign($privateKey, $msg) == $expectedSig; 
  289.  
  290. private function jwsAuthError($reason) { 
  291. throw new Simplify_AuthenticationException("JWS authentication failure: " . $reason); 
  292.  
  293. private function jwsVerifyTimestamp($ts) { 
  294. $now = round(microtime(true)); // Seconds 
  295. return abs($now - $ts / 1000) < self::JWS_MAX_TIMESTAMP_DIFF; 
  296.  
  297. private function isLiveKey($k) { 
  298. return strpos($k, "lvpb") === 0; 
  299.  
  300. private function jwsUrlSafeEncode64($s) { 
  301. return str_replace(array('+', '/', '='),  
  302. array('-', '_', ''),  
  303. base64_encode($s)); 
  304.  
  305. private function jwsUrlSafeDecode64($s) { 
  306.  
  307. switch (strlen($s) % 4) { 
  308. case 0: break; 
  309. case 2: $s = $s . "=="; 
  310. break; 
  311. case 3: $s = $s . "="; 
  312. break; 
  313. default: throw new InvalidArgumentException('incorrecly formatted JWS payload'); 
  314. return base64_decode(str_replace(array('-', '_'), array('+', '/'), $s)); 
  315.  
  316. private function buildOauthError($msg, $error, $error_description) { 
  317.  
  318. return array( 
  319. 'error' => array( 
  320. 'code' => 'oauth_error',  
  321. 'message' => $msg.', error code: '.$error.', description: '.$error_description.'' 
  322. );