/** * Run the security call and see what falls out. * * @param \Raml\SecurityScheme $securityscheme_obj The security scheme to process the call data for * @param \V1\APICall $apicall_obj The APICall object * * @return bool True on success, or false on fail */ public function run(\Raml\SecurityScheme $securityscheme_obj, \V1\APICall $apicall_obj) { /** * @link https://en.wikipedia.org/wiki/Basic_access_authentication */ $credentials = $apicall_obj->get_credentials(); // Make sure that we have the required data. if (empty($credentials['BASIC_USERNAME']) || empty($credentials['BASIC_PASSWORD'])) { return false; } $encoded_credentials = 'Basic ' . base64_encode($credentials['BASIC_USERNAME'] . ':' . $credentials['BASIC_PASSWORD']); $apicall_obj->set_header('Authorization', $encoded_credentials); return true; }
/** * Configure a dynamic call * * @return mixed The \V1\APICall object or the error array on fail */ public static function configure_call() { // Dynamic calls don't work through JS. if (\Session::get('public', true) === true) { return \Utility::format_error(400, \V1\Err::NO_JS_CALLS, \Lang::get('v1::errors.no_js_calls')); } // We need API configuration data. if (!is_array(\V1\APIRequest::get('configure', false))) { return \Utility::format_error(400, \V1\Err::MISSING_CONFIGURE, \Lang::get('v1::errors.missing_configure')); } // For some reason we can't parse the RAML, so we throw a 500 error. if (($api_def = \V1\RAML::parse()) === false) { return \Utility::format_error(500); } elseif (is_array($api_def)) { // Specific error return $api_def; } if (is_string($uri = \V1\APIRequest::get('configure.uri')) && is_string($method = \V1\APIRequest::get('configure.method'))) { $api_call = null; // Custom calls if (\V1\APIRequest::get('api') === 'custom') { if (is_string($url = \V1\APIRequest::get('configure.url')) && !empty($url)) { $api_call = \V1\APICall::forge($api_def, $method, $uri, null, true, $url); } return \Utility::format_error(400, \V1\Err::NO_URL, \Lang::get('v1::errors.no_url')); } $api_data = \V1\Model\APIs::get_api(); try { // Is it a valid resource? $api_def->getResourceByUri($uri)->getMethod($method); // We'll validate the call unless both the API provider and calling script deny that protection. $custom_dynamic = false; if ($api_data['force_validation'] === 0 && \V1\APIRequest::get('no-validate', false) === true) { $custom_dynamic = true; } $api_call = \V1\APICall::forge($api_def, $method, $uri, null, $custom_dynamic); } catch (\Raml\Exception\BadParameter\ResourceNotFoundException $e) { // Does the API Provider allow for unconfigured static calls on their server? if ($api_data['allow_custom_dynamic'] === 1) { $api_call = \V1\APICall::forge($api_def, $method, $uri, null, true); } } if (is_object($api_call)) { if (!empty($api_call->get_errors())) { // Errors from \APICall return $api_call->get_errors(); } else { // Return the \APICall object return $api_call; } } } // Not found return \Utility::format_error(400, \V1\Err::BAD_DYNAMIC, \Lang::get('v1::errors.bad_dynamic')); }
/** * Process the request data to include the new headers. * * @param string $request The entire request to the remote server including headers and the body * @return string The altered $request data */ public function before_send($request) { /** * @TODO Finish processing the HA2 data, then form the header. Check if APICall set a * header with the same name, and if so, overwrite it in the request string. */ $request_parts = explode("\r\n\r\n", $request); $uri = $this->apicall->get_uri(); $method = \Str::upper($this->apicall->get_method()); /** * HA2 */ if (empty($this->qop) || \Str::lower($this->qop === 'auth')) { $this->ha2 = md5($method . ':' . $uri); } else { /** * If we don't have a body, then we hash null. * * @link http://curl.haxx.se/mail/tracker-2013-06/0083.html */ $body = empty($request_parts[1]) ? 'd41d8cd98f00b204e9800998ecf8427e' : md5($request_parts[1]); $this->ha2 = md5($method . ':' . $uri . ':' . $body); } /** * RESPONSE */ if (empty($this->qop)) { $response = md5($this->ha1 . ':' . $this->nonce . ':' . $this->ha2); } else { $response = md5($this->ha1 . ':' . $this->nonce . ':' . $this->nonce_count . ':' . $this->cnonce . ':' . $this->qop . ':' . $this->ha2); } // Protect against CRLF, and add the header. $auth_header = str_replace("\r\n", '', 'Authorization: Digest username="******",realm="' . $this->realm . '",nonce="' . $this->nonce . '",uri="' . $uri . '",qop=' . $this->qop . ',nc=' . $this->nonce_count . ',cnonce="' . $this->cnonce . '",response="' . $response . '",opaque="' . $this->opaque . '"'); $request_parts[0] .= "\r\n" . $auth_header; $request = implode("\r\n\r\n", $request_parts); return $request; }
/** * Call the remote API server * * @param \V1\APICall $apicall_obj The APICall object we're using to make calls * @return array The array of data ready for display (Response array or error array) */ public function make_the_call(\V1\APICall $apicall_obj) { /** * RUNCLE RICK'S RAD RUN CALL :) */ $api = \V1\Model\APIs::get_api(); $account = \V1\Model\Account::get_account(); /* * When we make a call from the local server, we'll get the localhost IP. If that's the case, * we'll set our public IP. DO NOT use X-Forwarded-For in the request headers to us. It's unreliable. * We'll still set our X-Forwarded-For in case the API provider wishes to use it. */ $forwarded_for = \Input::real_ip('0.0.0.0', true); if ($internal_call = \Utility::is_internal_call()) { $forwarded_for = \Config::get('engine.call_test_ip'); } /* * Add our own headers to allow for authenticating our server and customers. We overwrite any * of these headers that were specified by the API Provider through RAML, or by the developer * through thier configuration. */ $headers = static::get_headers($apicall_obj->get_headers()); $call = array('url' => $apicall_obj->get_url(), 'method' => $apicall_obj->get_method(), 'headers' => $headers, 'body' => $apicall_obj->get_method_params(), 'body-type' => $apicall_obj->get_body_type()); if (\Fuel::$env !== 'production' && \Config::get('engine.dev_disable_live_calls', false) === true) { /** * In dev mode we can disable calls to the remote server. Feel free to change the * dummy response to whatever you'd like to. */ $response = array('status' => 200, 'headers' => array('X-Dev-Mode' => 'Dummy header'), 'body' => array('dummy_key' => 'dummy_val')); return \Utility::format_response(200, $response); } else { /* * We'll see if anyone got a cached entry into our system while we were configuring stuff. * That way we'll save time. */ if (\V1\APIRequest::is_static() && is_array($cached_data = \V1\Call\StaticCall::get_call_cache())) { // Return the response-formatted data from the cached entry. return $cached_data; } $queued = \V1\Socket::forge()->queue_call(\V1\APIRequest::get('api'), $call, $apicall_obj); } // Non-Data Calls grab the request right away. if (\Session::get('data_call', false) === false) { if ($queued === false) { // Server unavailable return \Utility::format_error(503, \Err::SERVER_ERROR, \Lang::get('v1::errors.remote_unavailable')); } // Pull the results. $result = \V1\Socket::forge()->get_results(); if (is_array($result)) { // We only have one call. return $result[\V1\APIRequest::get('api')][0]; } else { // If the request failed with false, it means that all streams timed out. return \Utility::format_error(500); } } $dc_response = array('status' => 200, 'headers' => array(), 'body' => \V1\Constant::QUEUED_CALL); // In Data Call mode we just signify that we've queued the call. return \Utility::format_response(200, $dc_response); }
/** * Run the request * * @param \Raml\SecurityScheme $securityscheme_obj The security scheme to process the call data for * @param \V1\APICall $apicall_obj The APICall object * * @return mixed The object we just completed or an array describing the next step in the security process */ public function run(\Raml\SecurityScheme $securityscheme_obj, \V1\APICall $apicall_obj) { $settings = $securityscheme_obj->getSettings(); $credentials = $apicall_obj->get_credentials(); // Save the credentials \V1\Keyring::set_credentials($credentials); /** * By default we'll return the response from the authentication request so that it's meaningful. * However, in doing so, we'll need to block the main request, so developers may set this flag * to ignore the authentication, signifying that they've already got the information they needed * from it. * * NOTE: This security method is meant as a basic way to catch security methods we otherwise * haven't implemented in our system. Take it for what it's worth. * * @TODO When using this security method, skip processing the APICall object for a speedup. */ if (!empty($credentials['CUSTOM_IGNORE_AUTH'])) { return true; } // Remove unused credentials so as not to replace bad variables in the template. foreach ($credentials as $variable => $entry) { if (strpos($variable, 'CUSTOM_') !== 0) { unset($credentials[$variable]); } } // We need the method or we'll fail the call. if (empty($settings['method'])) { $this->error = true; return $this; } // Normalize the data into arrays. $described_by = $securityscheme_obj->getDescribedBy(); $headers = $this->get_param_array($described_by->getHeaders()); $query_params = $this->get_param_array($described_by->getQueryParameters()); $bodies = $described_by->getBodies(); $method = \Str::upper($settings['method']); $url = $settings['url']; // Grab the body if we have one, and the method supports one. $body = null; $body_type = null; if (count($bodies) > 0 && !in_array($method, array('GET', 'HEAD'))) { reset($bodies); $body_type = key($bodies); $body = $bodies[$body_type]->getExamples()[0]; } /** * NOTE: These replacements may ruin the formatting or allow for people to inject data into them. * API Providers should be aware of that possibility. * * @TODO In the future, we can consider implementing checking to verify that people aren't sending * crap data through the system. */ $headers = $this->remove_cr_and_lf($this->replace_variables($headers, $credentials)); $query_params = $this->remove_cr_and_lf($this->replace_variables($query_params, $credentials)); $body = $this->replace_variables($body, $credentials); if (!empty($query_params)) { $query_string = http_build_query($query_params, null, '&'); if (strpos($url, '?') === false) { $url .= '?' . $query_string; } else { $url .= '&' . $query_string; } } /** * RUNCLE RICK'S RAD RUN CALLS (The second coming!) */ $curl = \Remote::forge($url, 'curl', $method); // Set the headers $headers = \V1\RunCall::get_headers($headers); foreach ($headers as $header_name => $header_value) { $curl->set_header($header_name, $header_value); } // Return the headers $curl->set_option(CURLOPT_HEADER, true); // If we need a body, set that. if (!empty($body) && !in_array($method, array('GET', 'HEAD'))) { $curl->set_header('Content-Type', $body_type); $curl->set_params($body); } // Run the request try { $response = $curl->execute()->response(); } catch (\RequestStatusException $e) { $response = \Remote::get_response($curl); } catch (\RequestException $e) { $this->error = true; return $this; } // Set the usage stats, and format the response return \V1\Socket::prepare_response(array('status' => $response->status, 'headers' => $response->headers, 'body' => $response->body)); }
/** * Try to get an \APICall object * * @param string $call The static call name, or null if we're making a dynamic call. * @return mixed The \APICall object on success, or false if an error occurred. */ protected static function apicall_object($call = null) { // For some reason we can't parse the RAML, so we throw a 500 error. if (($api_def = \V1\RAML::parse()) === false) { return \Utility::format_error(500); } elseif (is_array($api_def)) { // Specific error return $api_def; } $all_calls = (array) $api_def->getResourcesAsUri(); $all_calls = reset($all_calls); $api_call = null; // Loop through every possible URI on the API foreach ($all_calls as $uri => $call_data) { // GET /res/name $uri_explode = explode(' ', $uri); // Is it the static call we need? if (($call_uri = str_replace('/{{static-calls}}' . $call, '', $uri_explode[1])) !== $uri_explode[1]) { /* * Static calls only have one method, so since it matches the resource, we'll pass along * the method it uses. */ $api_call = \V1\APICall::forge($api_def, $uri_explode[0], $call_uri, $uri_explode[1]); break; } } if (is_object($api_call)) { if (!empty($api_call->get_errors())) { // Errors from \APICall return $api_call->get_errors(); } else { // Return the \APICall object return $api_call; } } return false; }
/** * Run the request * * @param \Raml\SecurityScheme $securityscheme_obj The security scheme to process the call data for * @param \V1\APICall $apicall_obj The APICall object * * @return mixed The object we just completed or an array describing the next step in the security process */ public function run(\Raml\SecurityScheme $securityscheme_obj, \V1\APICall $apicall_obj) { $settings = $securityscheme_obj->getSettings()->asArray(); $credentials = $apicall_obj->get_credentials(); $settings['authorization'] = empty($settings['authorization']) ? 'header' : \Str::lower($settings['authorization']); // Verify that we have the required credentials for the request. if (empty($credentials['OAUTH_CONSUMER_KEY']) || empty($credentials['OAUTH_CONSUMER_SECRET']) || empty($credentials['OAUTH_USER_ID'])) { $this->error = true; return $this; } // Store the proper credentials in the DB. $this->store_credentials($credentials); // Pull data from the cache for the current request, allowing for multiple authentications for the customer. $this->cache_id = hash('sha256', $credentials['OAUTH_CONSUMER_KEY'] . $credentials['OAUTH_CONSUMER_SECRET'] . $credentials['OAUTH_USER_ID']); $credentials = array_replace($this->get_cache(), $credentials); // Where should we set the authorization data? switch ($settings['authorizeLocation']) { case 'header': $authorize_location = OAUTH_AUTH_TYPE_AUTHORIZATION; break; case 'query': $authorize_location = OAUTH_AUTH_TYPE_URI; break; case 'body': $authorize_location = OAUTH_AUTH_TYPE_FORM; break; case 'none': $authorize_location = OAUTH_AUTH_TYPE_NONE; break; } try { // Create the PECL installed OAuth object. $oauth = new \OAuth($credentials['OAUTH_CONSUMER_KEY'], $credentials['OAUTH_CONSUMER_SECRET'], $settings['signatureMethod'], $authorize_location); if (\Fuel::$env !== 'production') { $oauth->enableDebug(); } if (empty($credentials['OAUTH_ACCESS_TOKEN']) || empty($credentials['OAUTH_ACCESS_TOKEN_SECRET'])) { // Get our access token and secret. if (($credentials = $this->get_access_tokens($oauth, $settings, $credentials)) === false) { $this->error = true; return $this; } // Authentication of my second leg (Yup. It's hairy, so it must be mine.) if (!empty($credentials['errors'])) { return $credentials; } } $oauth->setToken($credentials['OAUTH_ACCESS_TOKEN'], $credentials['OAUTH_ACCESS_TOKEN_SECRET']); // Collect parameters to build our signature $params = null; if ($apicall_obj->get_body_type() === 'application/x-www-form-urlencoded') { // If we need to handle string bodies later, we will. if (is_array($apicall_obj->get_method_params())) { $params = http_build_query($apicall_obj->get_method_params(), null, '&', PHP_QUERY_RFC3986) . '&'; } } $params .= http_build_query($apicall_obj->get_query_params(), null, '&') . '&' . ($params .= http_build_query($apicall_obj->get_headers(), null, '&')); $header = $oauth->getRequestHeader($apicall_obj->get_method(), $apicall_obj->get_url(), $params); $apicall_obj->set_header('Authorization', $header); return true; } catch (\OAuthException $e) { // Something went wrong, so destroy the cache so it can get fixed. $this->delete_cache(); // Let the script automatically continue searching for security methods. $this->error = true; return $this; } }
/** * Queue a call * * @param string $api The name of the API we're calling * @param array $call_data The array of call data for the call * @param \V1\APICall $apicall_obj The APICall object for the current call * * @return bool True on success, or false on fail */ public function queue_call($api, array $call_data, \V1\APICall $apicall_obj) { // We need a valid URL if (($parsed_url = parse_url($call_data['url'])) === false || empty($parsed_url['scheme']) || empty($parsed_url['host'])) { $this->streams[$api][] = \Utility::format_error(500); return false; } /** * Configure */ $parsed_url['path'] = empty($parsed_url['path']) ? '/' : $parsed_url['path']; $parsed_url['query'] = empty($parsed_url['query']) ? null : '?' . $parsed_url['query']; $call_data['method'] = empty($call_data['method']) ? 'GET' : \Str::upper($call_data['method']); $stream_scheme = null; if ($parsed_url['scheme'] === 'https') { $parsed_url['port'] = empty($parsed_url['port']) ? '443' : $parsed_url['port']; $stream_scheme = 'ssl://'; } else { $parsed_url['port'] = empty($parsed_url['port']) ? '80' : $parsed_url['port']; } $opts['http'] = array('method' => $call_data['method'], 'ignore_errors' => true, 'protocol_version' => 1.1); /** * Request */ $request = $call_data['method'] . " " . $parsed_url['path'] . $parsed_url['query'] . " HTTP/1.1\r\n"; $request .= "Host: " . $parsed_url['host'] . ":" . $parsed_url['port'] . "\r\n"; $request .= "Accept-Encoding: gzip, deflate\r\n"; // User headers if (!empty($call_data['headers']) && is_array($call_data['headers'])) { foreach ($call_data['headers'] as $header_name => $header_val) { $request .= $header_name . ": " . $header_val . "\r\n"; } } /** * Body */ if (!empty($call_data['body-type']) && !empty($call_data['body']) && !in_array($call_data['method'], array('GET', 'HEAD'))) { $build_body = static::build_body($call_data['method'], $call_data['body-type'], $call_data['body']); if (is_array($build_body)) { // Content-Type and Content-Length headers and our body $request .= $build_body['header'] . "\r\n"; $request .= "Content-Length: " . strlen($build_body['body']) . "\r\n\r\n"; $request .= $build_body['body'] . "\r\n"; } else { // We need to post, but we can't. if (\V1\APIRequest::is_static() === true) { $this->streams[$api][] = \Utility::format_error(400, \V1\Err::BAD_FORMAT, \Lang::get('v1::errors.bad_format_static')); } $this->streams[$api][] = \Utility::format_error(400, \V1\Err::BAD_FORMAT, \Lang::get('v1::errors.bad_format')); return false; } } else { // Finish our request $request .= "\r\n"; } /** * CALLBACKS */ if (!empty($security_calls = $apicall_obj->get_security_calls())) { foreach ($security_calls as $security_call) { // Process the request data as needed. $request = $security_call->before_send($request); } } $context = stream_context_create($opts); try { $stream = stream_socket_client($stream_scheme . $parsed_url['host'] . ":" . $parsed_url['port'], $errno, $errstr, \Config::get('v1::socket.timeout', 5), STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_CONNECT, $context); } catch (\Exception $e) { // Server unavailable $this->streams[$api][] = \Utility::format_error(503, \Err::SERVER_ERROR, \Lang::get('v1::errors.remote_unavailable')); return false; } $this->api_request[$api][] = \V1\APIRequest::get(); $this->is_static[$api][] = \V1\APIRequest::is_static(); $this->api_call[$api][] = $apicall_obj; if ($stream !== false) { fwrite($stream, $request); $this->streams[$api][] =& $stream; return true; } else { // Server unavailable $this->streams[$api][] = \Utility::format_error(503, \Err::SERVER_ERROR, \Lang::get('v1::errors.remote_unavailable')); return false; } }