/** * The main entry point for V1 of the API engine * * @return string The JSON encoded string for the loader to parse */ public function action_index() { // Set up our APIRequest object if (is_array($setup = \V1\APIRequest::setup())) { return $this->response_format(); } // Validate the request if (is_array($validate_request = \V1\ValidateRequest::run())) { return $this->response_format($validate_request); } // Validate a Data Call if (\V1\APIRequest::get('data-call', false) !== false) { if (is_array($data_call = \V1\Call\DataCall::run()) || is_string($data_call)) { // HTML only Data Calls if (\Session::get('response_format', false) === 'html' && is_string($data_call)) { return $data_call; } // Non-HTML Data Calls return $this->response_format($data_call); } } // Are we processing an API call? if (\V1\APIRequest::get('api', false) !== false) { $runcall = new \V1\RunCall(); if (is_array($response = $runcall->ignite())) { return $this->response_format($response); } } // Nothing happened, so return an error. return $this->response_format(\Utility::format_error(500)); }
public function test_format_error() { $custom_response = array('errors' => array('code' => 500, 'type' => 'server error', 'message' => 'Test server error')); $default_response = array('errors' => array('code' => 500, 'type' => 'server error', 'message' => 'Internal Server Error')); // Custom $this->assertSame($custom_response, \Utility::format_error(500, \Err::SERVER_ERROR, 'Test server error')); // Default $this->assertSame($default_response, \Utility::format_error(500)); }
/** * 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')); }
/** * Load every call to the API with this method. * * @return void * @access public */ public function action_index() { // Profile the loader \Profiler::mark('Start of loader\'s action_index() function'); \Profiler::mark_memory($this, 'Start of loader\'s action_index() function'); // Make sure we aren't processing crap. if (in_array($this->format, array('csv', 'php', 'serialize'))) { $this->format = 'json'; } // For some reason this value is quoted when set to html. if (\Input::post('format') === '"html"') { $this->format = 'html'; } // Cleanse the session to keep things stable. \Session::destroy(); // For error handling \Session::set('response_format', $this->format); // External error processing through Apache if (\Uri::segment(1) === 'error' && is_numeric(\Uri::segment(2)) && strlen(\Uri::segment(2)) === 3) { return $this->response(\Utility::format_error(\Uri::segment(2))); } // /loader/index/error/404 style (Due to routing) if (substr_count(\Uri::current(), 'loader/index/error') === 1 && is_numeric(\Uri::segment(4)) && strlen(\Uri::segment(4)) === 3) { return $this->response(\Utility::format_error(\Uri::segment(4))); } // We need a version number if (empty(\Uri::segment(1)) || \Module::exists(\Uri::segment(1)) === false) { $error_data = \Utility::format_error(400, \Err::BAD_OR_NO_VERSION, \Lang::get('errors.bad_version')); return $this->response($error_data, 400); } // We need a request. if (empty(\Input::post()) || \Input::method() !== 'POST') { $error_data = \Utility::format_error(405, null, \Lang::get('errors.no_request')); return $this->response($error_data, 405); } // Pass the request to the proper API version request handler. (Module) if (!empty(\Input::post())) { \Module::load(\Uri::segment(1)); $response = \Request::forge(\Uri::segment(1) . '/index', false)->execute()->response->body; // HTML only Data Calls if (is_string($response)) { return $this->response($response, 200); } return $this->response($response[0], $response[1]); } }
/** * Validate the request to make sure we have trusted and sufficient data. * * @return mixed True on success, or an array of error data on fail */ public static function run() { /** * Verify that we have at least an "api" or "data-call" token. */ if (\V1\APIRequest::get('api', false) === false && \V1\APIRequest::get('data-call', false) === false) { return \Utility::format_error(400, \V1\Err::BAD_BODY, \Lang::get('v1::errors.bad_body')); } /** * Easter egg processing plant */ if (\V1\APIRequest::get('api') === 'I\'m a teapot') { return \Utility::format_error(418, \V1\Err::IM_A_TEAPOT, str_replace("\t", '', \Lang::get('v1::errors.im_a_teapot'))); } /** * AUTHORIZATION */ // Once we've authenticated to start running calls from one Data Call, we don't authenticate again. if (\Session::get('data_call', false) === false) { // If they failed to authenticate, then issue a 401 unauthorized error. if (\V1\Account::authenticate() === false) { // Log the failure. \Log::logger('INFO', 'AUTHORIZE:FAIL', \Lang::get('log.authorize_fail'), __METHOD__, array('consumer_key' => \Session::get('consumer_key', 'NOT SET'), 'public_mode' => \Session::get('public', 'NOT SET'))); return \Utility::format_error(401); } // Log the success. \Log::logger('INFO', 'AUTHORIZE:SUCCESS', \Lang::get('log.authorize_success'), __METHOD__, array('consumer_key' => \Session::get('consumer_key', 'NOT SET'), 'public_mode' => \Session::get('public', 'NOT SET'))); /** * DOWNGRADE PROCESSING */ \V1\Account::downgrade(); } /** * GLOBAL LIMITS */ if (static::check_global_limits() === false) { return \Utility::format_error(429, \V1\Err::MAXED_OUT_LIMITS, \Lang::get('v1::errors.maxed_out_limits')); } return true; }
/** * Parse a RAML file * * @param string $raml_string The RAML string to parse * @param string $root_dir The root directory to parse !include files (Defaults to the "raml" * directory for the module.) * @param bool $parse_schemas True to parse included RAML files, or false not to (Defaults to true) * * @return mixed The \Raml\APIDefinition object, or false if the RAML string didn't exist */ public static function parse($raml_string = null, $root_dir = null, $parse_schemas = true) { // Find the RAML string. if (empty($raml_string) && ($raml_string = static::get_raml_string()) === false) { return false; } // Use the default root directory for RAML files. if (empty($root_dir)) { $root_dir = __DIR__ . DS . '..' . DS . 'raml'; } // Build the settings object. $parser_config_obj = new \Raml\ParseConfiguration(); if ($parse_schemas === true) { $parser_config_obj->enableSchemaParsing(); } else { $parser_config_obj->disableSchemaParsing(); } /* * Don't create security settings parser objects. They restrict settings to only those the parsers * specify. */ $parser_config_obj->disableSecuritySchemeParsing(); $parser = new \Raml\Parser(null, null, null, $parser_config_obj); /** * Prevent the parser from automatically merging security scheme data with the method. * * @link https://github.com/alecsammon/php-raml-parser/issues/68 */ //$parser->setMergeSecurity(); try { $api_def = $parser->parseFromString($raml_string, $root_dir); } catch (\Raml\Exception\InvalidSchemaTypeException $e) { return \Utility::format_error(500, \V1\Err::INVALID_SCHEMA_TYPE, \Lang::get('v1::errors.invalid_schema_type')); } return $api_def; }
/** * 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); }
/** * Check the speed limits of an API to make sure that the account isn't calling the API more than it's * allowed to in the specified time frame. * * @return mixed True if the call is within the speed limits, or the error array if it isn't */ private static function check_usage_limits() { $api_data = \V1\Model\APIs::get_api(\V1\APIRequest::get('api')); $account_data = \V1\Model\Account::get_account(); $package_limits = \V1\RAML::parse_package_limits(); /* * Are there any limits on the API for our package level? API limits are imposed * by Bit API Hub, not the API provider. It helps to limit the amount of bandwidth * we allot for each call. The heavier the call, the less calls people can make. */ if (is_array($package_limits) && array_key_exists('level' . $account_data['access_level'], $package_limits) && !empty($usage = \V1\Usage::get_usage(\V1\APIRequest::get('api')))) { // Loop the allotments of API calls per time period foreach ($package_limits['level' . $account_data['access_level']] as $time_period => $allotment) { // If we have a valid log for the time period, and it's already maxed out, fail the check. if (isset($usage[$time_period]) && $usage[$time_period]['count'] == $allotment && \Utility::subtract_seconds($time_period) <= $usage[$time_period]['ts'] && $usage[$time_period]['ts'] <= time()) { return \Utility::format_error(429, \V1\Err::TOO_MANY_REQUESTS, \Lang::get('v1::errors.too_many_requests', array('allotment' => number_format($allotment), 'period' => $time_period))); } } } // We're not breaking the speed limit, so shush. return true; }
/** * Populate the settings data. * * @return mixed The error array if we submitted serialized data, or void if everything went as planned. */ public static function setup() { $post_data = \Session::get('data_call', false) === false ? \Input::post() : \Session::get('posted_data', array()); $settings_array = array(); foreach ($post_data as $input_key => $input_val) { $decoded = is_string($input_val) ? json_decode($input_val, true) : $input_val; $input_val = empty($decoded) ? $input_val : $decoded; // No serialized input due to security issues if (static::is_serialized_recursive($input_val)) { return \Utility::format_error(400); } $settings_array[$input_key] = $input_val; } if (isset($post_data['data-call'])) { static::instance()->data_call = $settings_array; } static::instance()->settings = $settings_array; static::instance()->post_data = $post_data; return true; }
/** * Prevent CRLF attacks * * @param array $header_array The array of headers * @return boolean True if we're safe, false if not */ protected function check_crlf(array $header_array) { foreach ($header_array as $header_name => $header_val) { if (substr_count($header_val, "\n") > 0 || substr_count($header_val, "\r") > 0 || substr_count($header_val, '\\n') > 0 || substr_count($header_val, '\\r') > 0) { $this->errors = \Utility::format_error(400, \V1\Err::NEW_LINE_HEADER, \Lang::get('v1::errors.new_line_header')); return false; } } return true; }
/** * 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 Data Call * * @return mixed The array of responses and template data, or the HTML template if the format is "html" */ public static function run() { if (\Session::get('public', true) === true) { return \Utility::format_error(400, \V1\Err::NO_JS_CALLS, \Lang::get('v1::errors.no_js_calls')); } // Make sure the Data Call exists, and is part of their account. if (empty($data_call_data = \V1\Model\DataCalls::get_data_call())) { return \Utility::format_error(404, \V1\Err::BAD_DATA_CALL, \Lang::get('v1::errors.bad_data_call')); } $account_data = \V1\Model\Account::get_account(); // Make sure they are allowed to access it. if ($account_data['access_level'] < $data_call_data['min_access_level']) { return \Utility::format_error(402, \V1\Err::UPGRADE_REQUIRED, \Lang::get('v1::errors.upgrade_required')); } // Make sure that the Data Call is enabled or callable. if ($data_call_data['active_level'] === 0 && ($account_data['can_run_inactive'] === 0 && $account_data['id'] !== $data_call_data['account_id'])) { return \Utility::format_error(403, \V1\Err::DISABLED_DATA_CALL, \Lang::get('v1::errors.disabled_data_call')); } // Custom Data Calls allow the user to send us their workload (call script) and we'll process it for them. if (\V1\APIRequest::get('data-call', false) === 'custom') { $call_script = \V1\APIRequest::get('call-script', false); } else { $call_script = json_decode($data_call_data['call_script'], true); } // Make sure we have a call script. if (empty($call_script) || !is_array($call_script)) { return \Utility::format_error(503, \V1\Err::DATA_CALL_MISCONFIG, \Lang::get('v1::errors.data_call_misconfig')); } // Free accounts may not change their Data Calls, as they may only use public data calls. if ($account_data['access_level'] > 1 && is_array($call_options = \V1\APIRequest::data_call('call-options', false))) { $call_script = array_replace_recursive($call_script, $call_options); } /* * Set the Data Call flag to bypass things that no longer pertain to further calls, such as * authentication to Bit API Hub. */ \Session::set('data_call', true); $response = array(); $template_data = null; foreach ($call_script as $key => $call) { // If we have template data for widgets, gadgets, graphs, and charts, oh my, then we'll use it later. if ($key === 'template') { $template_data = $call; continue; } // We need a name. Even custom calls use the "custom" API. if (empty($call['api'])) { continue; } // Set the post data as defined in our script. \Session::set('posted_data', $call); // Make the call. $api_call = \Request::forge('v1/index', false)->execute()->response->body; // Bad decode if (empty($api_call)) { $response[$call['api']][] = \Utility::format_error(500); } // The response cometh forth, not stuck in zein queue. if (!empty($api_call[0]['response']['body']) && $api_call[0]['response']['body'] === \V1\Constant::QUEUED_CALL) { // Keep the order of calls right proper. :P $response[$call['api']][] = \V1\Constant::QUEUED_CALL; } else { // We have our response, so we set that now. $response[$call['api']][] = $api_call[0]; } } // If the customer doesn't need any response data, then we don't make them wait while we retrieve the results. if (\V1\APIRequest::data_call('no-response', false) === true) { $response = array('status' => 200, 'headers' => array(), 'body' => \Lang::get('v1::response.done')); return \Utility::format_response(200, $response); } // Check for responses. $call_queue = \V1\Socket::forge()->get_results(); // If we have queued responses, and possibly place holders, then we'll loop. if (!empty($call_queue) && !empty($response)) { // Let's loop. :) foreach ($call_queue as $api_name => $call_number_data) { /* * Somehow we don't need the response. Odd... I don't know why they bother giving me this stuff * if they don't want me to use it. */ if (empty($response[$api_name])) { continue; } // Find the next queued placeholder. $queued_placeholder = array_search(\V1\Constant::QUEUED_CALL, $response[$api_name]); // If we have a placeholder, then put it's value in place. if ($queued_placeholder !== false) { $response[$api_name][$queued_placeholder] = $call_number_data[(int) key($call_number_data)]; } } } // If we have template data to display, then format that now. if (!empty($template_data)) { // We only want the template we just compiled, so return that. if (\Session::get('response_format') === 'html') { return static::process_template($template_data, $response, true); } // Set the template to the array of template data. $response['template'] = static::process_template($template_data, $response); return \Utility::format_response(200, $response); } else { // No template data, so just return the responses. return \Utility::format_response(200, $response); } }
/** * Grab the API access token and key, and return the array of credentials * * @param \OAuth $oauth The OAuth object we're using for authentication * @param array $settings The array of RAML settings * @param array $credentials The array of credentials for our call * * @return array The array of credentials, or an error array when the customer must manually * validate for the second leg, or false on fail */ private function get_access_tokens(\OAuth $oauth, array $settings, array $credentials) { $callback_url = \Config::get('engine.oauth_10a_callback_url', null); if (!empty($settings['callback'])) { $callback_url = $settings['callback']; } // Grab the request token if we didn't just authorize the request. if (empty($credentials['OAUTH_REQUEST_TOKEN']) && empty($credentials['OAUTH_REQUEST_TOKEN_SECRET'])) { try { $request_token_info = $oauth->getRequestToken($settings['requestTokenUri'], $callback_url, $settings['requestTokenMethod']); } catch (\OAuthException $e) { // Something went wrong, so destroy the cache, and return false so it can get fixed. $this->delete_cache(); return false; } $credentials['OAUTH_REQUEST_TOKEN'] = $request_token_info['oauth_token']; $credentials['OAUTH_REQUEST_TOKEN_SECRET'] = $request_token_info['oauth_token_secret']; } if (empty($credentials['OAUTH_VERIFIER'])) { $credentials['OAUTH_VERIFIER'] = null; } // Three legged auth requires manual validation. if ($settings['legs'] === 3) { if (empty($credentials['OAUTH_REQUEST_TOKEN']) || empty($credentials['OAUTH_REQUEST_TOKEN_SECRET']) || empty($credentials['OAUTH_VERIFIER'])) { if (strpos($settings['authorizationUri'], '?') === false) { $settings['authorizationUri'] .= '?'; } else { $settings['authorizationUri'] .= '&'; } // Save the request tokens. $this->set_cache($credentials); /* * Only non-static calls may show the authorization URL so as to not mislead people into * authorizing their accounts for everyone connected to the API hub. */ if (\V1\APIRequest::is_static() === false) { // Tell them were to go. return \Utility::format_error(401, \V1\Err::OAUTH1_AUTHORIZE, \Lang::get('v1::errors.oauth1_authorize', array('url' => $settings['authorizationUri'] . 'oauth_token=' . $request_token_info['oauth_token']))); } else { return false; } } } $oauth->setToken($credentials['OAUTH_REQUEST_TOKEN'], $credentials['OAUTH_REQUEST_TOKEN_SECRET']); try { $access_token_info = $oauth->getAccessToken($settings['tokenCredentialsUri'], null, $credentials['OAUTH_VERIFIER'], $settings['tokenCredentialsMethod']); } catch (\OAuthException $e) { // Something went wrong, so destroy the cache, and return false so it can get fixed. $this->delete_cache(); return false; } // Clean up our data, and set the access key and secret in the DB. unset($credentials['OAUTH_REQUEST_TOKEN']); unset($credentials['OAUTH_REQUEST_TOKEN_SECRET']); unset($credentials['OAUTH_VERIFIER']); $credentials['OAUTH_ACCESS_TOKEN'] = $access_token_info['oauth_token']; $credentials['OAUTH_ACCESS_TOKEN_SECRET'] = $access_token_info['oauth_token_secret']; // Keep the credentials cached $this->set_cache($credentials); return $credentials; }
public function response() { return new \Response(\Utility::prepare_response(\Utility::format_error(400)), 400); }
/** * Get the results from all of our API calls. * * @return mixed The array of response data, or false on fail */ public function get_results() { /** * NOTE: Do not use \V1\APIRequest from this point on, as Data Calls will only call this method * once, thus leaving us with the data from the last API call the Data Call ran. Instead, use * $this->api_request[$api][$api_call_number] */ $responses = array(); $sockets_array = array(); foreach ($this->streams as $api => $sockets) { // Make sure we only have resources foreach ($sockets as $socket_id => $socket_to_me_baby) { // May be an error if (!is_resource($socket_to_me_baby)) { $responses[$api][$socket_id] = false; continue; } $sockets_array[$socket_id] =& $this->streams[$api][$socket_id]; } $read =& $sockets_array; $write = null; $exception = null; // Nothing to process if (empty($sockets_array)) { $this->streams = array(); return false; break; } /** * NOTE: stream_select() will continually update the read stream! As the data comes in, it's made * available. That means that when you try to access data from the stream, the stream may not have * finished yet, so not all data will be present. Use the Content-Length header to figure out how * much data there is. */ stream_select($read, $write, $exception, \Config::get('v1::socket.timeout', 5)); if (count($read)) { foreach ($read as &$read_stream) { $id = array_search($read_stream, $sockets_array); $response = explode("\r\n\r\n", fread($read_stream, 8192)); // Make sure that we have all of the headers. while (empty($response[1])) { $response = $response[0] . fread($read_stream, 8192); $response = explode("\r\n\r\n", $response); } $headers = static::build_headers(explode("\r\n", $response[0])); unset($response[0]); $response[1] = implode("\r\n\r\n", $response); $status_code = $headers['BAH-STATUS']; unset($headers['BAH-STATUS']); // We need to know the length before we can start pulling. if (!empty($headers['Content-Length'])) { while (strlen($response[1]) < (int) $headers['Content-Length']) { $response[1] .= fread($read_stream, 8192); } // Decode the body if (!empty($headers['Content-Encoding'])) { $response[1] = static::decode_body($response[1], $headers); } } elseif (!empty($headers['Transfer-Encoding']) && $headers['Transfer-Encoding'] === 'chunked') { $lines = array_reverse(explode("\r\n", $response[1])); // Check if we've already finished pulling data. if (count($lines) < 3 || !(count($lines) >= 3 && strlen($lines[0]) === 0 && strlen($lines[1]) === 0 && strlen($lines[2]) === 1 && is_numeric($lines[2]) && (int) $lines[2] === 0)) { /** * @TODO If the chunking is corrupt, then the loop won't break, and fread will * try to pull data that it can't, and it'll make the script hang. Perhaps * there's a better way around the fact that we can't tell if a live socket is * at the end for some reason? */ while (1) { $response[1] .= fread($read_stream, 8192); // Check if we've finished the chunking. $lines = array_reverse(explode("\r\n", $response[1])); if (count($lines) >= 3 && strlen($lines[0]) === 0 && strlen($lines[1]) === 0 && strlen($lines[2]) === 1 && is_numeric($lines[2]) && (int) $lines[2] === 0) { break; } } } // Decode the body $response[1] = static::decode_body($response[1], $headers); } else { $response[1] = false; } // We won't allow remote APIs to send us a serialized response for security reasons. if ($response[1] === false || \Utility::is_serialized($response[1]) === true) { $responses[$api][$id] = \Utility::format_error(500, \V1\Err::BAD_RESPONSE, \Lang::get('v1::errors.bad_response')); continue; } $response_data = array('status' => $status_code, 'headers' => $headers, 'body' => $response[1]); /** * CALLBACKS */ $apicall_obj = $this->api_call[$api][$socket_id]; if (!empty($security_calls = $apicall_obj->get_security_calls())) { foreach ($security_calls as $security_call) { // Process the response data as needed. $response_data = $security_call->after_response($response_data); } } // Prepare the response to show the customer. $responses[$api][$id] = static::prepare_response($response_data, $this->api_request[$api][$id], $this->is_static[$api][$id]); fclose($read_stream); } // Avoid pointer location issues. unset($read_stream); } else { // A time-out means that *all* streams have failed to receive a response. $this->streams = array(); return false; break; } } // Return the responses after we reset the stack. $this->streams = array(); return $responses; }