authenticate ( $operation, $payload )

Authenticates a connection to Amazon S3. Do not use directly unless implementing custom methods for this class.

Access

public

Parameters

Parameter

Type

Required

Description

$operation

string

Required

The name of the bucket to operate on (S3 Only).

$payload

array

Required

An associative array of parameters for authenticating. See inline comments for allowed keys.

Returns

Type

Description

CFResponse

A CFResponse object containing a parsed HTTP response.

See Also

Source

Method defined in services/s3.class.php | Toggle source view (491 lines) | View on GitHub

public function authenticate($operation, $payload)
{
    /*
     * Overriding or extending this class? You can pass the following "magic" keys into $opt.
     *
     * ## verb, resource, sub_resource and query_string ##
     *     <verb> /<resource>?<sub_resource>&<query_string>
     *     GET /filename.txt?versions&prefix=abc&max-items=1
     *
     * ## versionId, uploadId, partNumber, response-* ##
     *     These don't follow the same rules as above, in that the they needs to be signed, while
     *     other query_string values do not.
     *
     * ## curlopts ##
     *     These values get passed directly to the cURL methods in RequestCore.
     *
     * ## fileUpload, fileDownload, seekTo ##
     *     These are slightly modified and then passed to the cURL methods in RequestCore.
     *
     * ## headers ##
     *     $opt['headers'] is an array, whose keys are HTTP headers to be sent.
     *
     * ## body ##
     *     This is the request body that is sent to the server via PUT/POST.
     *
     * ## preauth ##
     *     This is a hook that tells authenticate() to generate a pre-authenticated URL.
     *
     * ## returnCurlHandle ##
     *     Tells authenticate() to return the cURL handle for the request instead of executing it.
     */

    // Rename variables (to overcome inheritence issues)
    $bucket = $operation;
    $opt = $payload;

    // Validate the S3 bucket name
    if (!$this->validate_bucketname_support($bucket))
    {
        // @codeCoverageIgnoreStart
        throw new S3_Exception('S3 does not support "' . $bucket . '" as a valid bucket name. Review "Bucket Restrictions and Limitations" in the S3 Developer Guide for more information.');
        // @codeCoverageIgnoreEnd
    }

    // Die if $opt isn't set.
    if (!$opt) return false;

    $method_arguments = func_get_args();

    // Use the caching flow to determine if we need to do a round-trip to the server.
    if ($this->use_cache_flow)
    {
        // Generate an identifier specific to this particular set of arguments.
        $cache_id = $this->key . '_' . get_class($this) . '_' . $bucket . '_' . sha1(serialize($method_arguments));

        // Instantiate the appropriate caching object.
        $this->cache_object = new $this->cache_class($cache_id, $this->cache_location, $this->cache_expires, $this->cache_compress);

        if ($this->delete_cache)
        {
            $this->use_cache_flow = false;
            $this->delete_cache = false;
            return $this->cache_object->delete();
        }

        // Invoke the cache callback function to determine whether to pull data from the cache or make a fresh request.
        $data = $this->cache_object->response_manager(array($this, 'cache_callback'), $method_arguments);

        if ($this->parse_the_response)
        {
            // Parse the XML body
            $data = $this->parse_callback($data);
        }

        // End!
        return $data;
    }

    // If we haven't already set a resource prefix and the bucket name isn't DNS-valid...
    if ((!$this->resource_prefix && !$this->validate_bucketname_create($bucket)) || $this->path_style)
    {
        // Fall back to the older path-style URI
        $this->set_resource_prefix('/' . $bucket);
        $this->temporary_prefix = true;
    }

    // If the bucket name has periods and we are using SSL, we need to switch to path style URLs
    $bucket_name_may_cause_ssl_wildcard_failures = false;
    if ($this->use_ssl && strpos($bucket, '.') !== false)
    {
        $bucket_name_may_cause_ssl_wildcard_failures = true;
    }

    // Determine hostname
    $scheme = $this->use_ssl ? 'https://' : 'http://';
    if ($bucket_name_may_cause_ssl_wildcard_failures || $this->resource_prefix || $this->path_style)
    {
        // Use bucket-in-path method
        $hostname = $this->hostname . $this->resource_prefix . (($bucket === '' || $this->resource_prefix === '/' . $bucket) ? '' : ('/' . $bucket));
    }
    else
    {
        $hostname = $this->vhost ? $this->vhost : (($bucket === '') ? $this->hostname : ($bucket . '.') . $this->hostname);
    }

    // Get the UTC timestamp in RFC 2616 format
    $date = gmdate(CFUtilities::DATE_FORMAT_RFC2616, time());

    // Storage for request parameters.
    $resource = '';
    $sub_resource = '';
    $querystringparams = array();
    $signable_querystringparams = array();
    $string_to_sign = '';
    $headers = array(
        'Content-MD5' => '',
        'Content-Type' => 'application/x-www-form-urlencoded',
        'Date' => $date
    );

    /*%******************************************************************************************%*/

    // Do we have an authentication token?
    if ($this->auth_token)
    {
        $headers['X-Amz-Security-Token'] = $this->auth_token;
    }

    // Handle specific resources
    if (isset($opt['resource']))
    {
        $resource .= $opt['resource'];
    }

    // Merge query string values
    if (isset($opt['query_string']))
    {
        $querystringparams = array_merge($querystringparams, $opt['query_string']);
    }
    $query_string = $this->util->to_query_string($querystringparams);

    // Merge the signable query string values. Must be alphabetical.
    $signable_list = array(
        'partNumber',
        'response-cache-control',
        'response-content-disposition',
        'response-content-encoding',
        'response-content-language',
        'response-content-type',
        'response-expires',
        'uploadId',
        'versionId'
    );
    foreach ($signable_list as $item)
    {
        if (isset($opt[$item]))
        {
            $signable_querystringparams[$item] = $opt[$item];
        }
    }
    $signable_query_string = $this->util->to_query_string($signable_querystringparams);

    // Merge the HTTP headers
    if (isset($opt['headers']))
    {
        $headers = array_merge($headers, $opt['headers']);
    }

    // Compile the URI to request
    $conjunction = '?';
    $signable_resource = '/' . str_replace('%2F', '/', rawurlencode($resource));
    $non_signable_resource = '';

    if (isset($opt['sub_resource']))
    {
        $signable_resource .= $conjunction . rawurlencode($opt['sub_resource']);
        $conjunction = '&';
    }
    if ($signable_query_string !== '')
    {
        $signable_query_string = $conjunction . $signable_query_string;
        $conjunction = '&';
    }
    if ($query_string !== '')
    {
        $non_signable_resource .= $conjunction . $query_string;
        $conjunction = '&';
    }
    if (substr($hostname, -1) === substr($signable_resource, 0, 1))
    {
        $signable_resource = ltrim($signable_resource, '/');
    }

    $this->request_url = $scheme . $hostname . $signable_resource . $signable_query_string . $non_signable_resource;

    if (isset($opt['location']))
    {
        $this->request_url = $opt['location'];
    }

    // Gather information to pass along to other classes.
    $helpers = array(
        'utilities' => $this->utilities_class,
        'request' => $this->request_class,
        'response' => $this->response_class,
    );

    // Instantiate the request class
    $request = new $this->request_class($this->request_url, $this->proxy, $helpers, $this->credentials);

    // Update RequestCore settings
    $request->request_class = $this->request_class;
    $request->response_class = $this->response_class;
    $request->ssl_verification = $this->ssl_verification;

    // Pass along registered stream callbacks
    if ($this->registered_streaming_read_callback)
    {
        $request->register_streaming_read_callback($this->registered_streaming_read_callback);
    }

    if ($this->registered_streaming_write_callback)
    {
        $request->register_streaming_write_callback($this->registered_streaming_write_callback);
    }

    // Streaming uploads
    if (isset($opt['fileUpload']))
    {
        if (is_resource($opt['fileUpload']))
        {
            // Determine the length to read from the stream
            $length = null; // From current position until EOF by default, size determined by set_read_stream()

            if (isset($headers['Content-Length']))
            {
                $length = $headers['Content-Length'];
            }
            elseif (isset($opt['seekTo']))
            {
                // Read from seekTo until EOF by default
                $stats = fstat($opt['fileUpload']);

                if ($stats && $stats['size'] >= 0)
                {
                    $length = $stats['size'] - (integer) $opt['seekTo'];
                }
            }

            $request->set_read_stream($opt['fileUpload'], $length);

            if ($headers['Content-Type'] === 'application/x-www-form-urlencoded')
            {
                $headers['Content-Type'] = 'application/octet-stream';
            }
        }
        else
        {
            $request->set_read_file($opt['fileUpload']);

            // Determine the length to read from the file
            $length = $request->read_stream_size; // The file size by default

            if (isset($headers['Content-Length']))
            {
                $length = $headers['Content-Length'];
            }
            elseif (isset($opt['seekTo']) && isset($length))
            {
                // Read from seekTo until EOF by default
                $length -= (integer) $opt['seekTo'];
            }

            $request->set_read_stream_size($length);

            // Attempt to guess the correct mime-type
            if ($headers['Content-Type'] === 'application/x-www-form-urlencoded')
            {
                $extension = explode('.', $opt['fileUpload']);
                $extension = array_pop($extension);
                $mime_type = CFMimeTypes::get_mimetype($extension);
                $headers['Content-Type'] = $mime_type;
            }
        }

        $headers['Content-Length'] = $request->read_stream_size;
        $headers['Content-MD5'] = '';
    }

    // Handle streaming file offsets
    if (isset($opt['seekTo']))
    {
        // Pass the seek position to RequestCore
        $request->set_seek_position((integer) $opt['seekTo']);
    }

    // Streaming downloads
    if (isset($opt['fileDownload']))
    {
        if (is_resource($opt['fileDownload']))
        {
            $request->set_write_stream($opt['fileDownload']);
        }
        else
        {
            $request->set_write_file($opt['fileDownload']);
        }
    }

    $curlopts = array();

    // Set custom CURLOPT settings
    if (isset($opt['curlopts']))
    {
        $curlopts = $opt['curlopts'];
    }

    // Debug mode
    if ($this->debug_mode)
    {
        $curlopts[CURLOPT_VERBOSE] = true;
    }

    // Set the curl options.
    if (count($curlopts))
    {
        $request->set_curlopts($curlopts);
    }

    // Do we have a verb?
    if (isset($opt['verb']))
    {
        $request->set_method($opt['verb']);
        $string_to_sign .= $opt['verb'] . "\n";
    }

    // Add headers and content when we have a body
    if (isset($opt['body']))
    {
        $request->set_body($opt['body']);
        $headers['Content-Length'] = strlen($opt['body']);

        if ($headers['Content-Type'] === 'application/x-www-form-urlencoded')
        {
            $headers['Content-Type'] = 'application/octet-stream';
        }

        if (!isset($opt['NoContentMD5']) || $opt['NoContentMD5'] !== true)
        {
            $headers['Content-MD5'] = $this->util->hex_to_base64(md5($opt['body']));
        }
    }

    // Handle query-string authentication
    if (isset($opt['preauth']) && (integer) $opt['preauth'] > 0)
    {
        unset($headers['Date']);
        $headers['Content-Type'] = '';
        $headers['Expires'] = is_int($opt['preauth']) ? $opt['preauth'] : strtotime($opt['preauth']);
    }

    // Sort headers
    uksort($headers, 'strnatcasecmp');

    // Add headers to request and compute the string to sign
    foreach ($headers as $header_key => $header_value)
    {
        // Strip linebreaks from header values as they're illegal and can allow for security issues
        $header_value = str_replace(array("\r", "\n"), '', $header_value);

        // Add the header if it has a value
        if ($header_value !== '')
        {
            $request->add_header($header_key, $header_value);
        }

        // Generate the string to sign
        if (
            strtolower($header_key) === 'content-md5' ||
            strtolower($header_key) === 'content-type' ||
            strtolower($header_key) === 'date' ||
            (strtolower($header_key) === 'expires' && isset($opt['preauth']) && (integer) $opt['preauth'] > 0)
        )
        {
            $string_to_sign .= $header_value . "\n";
        }
        elseif (substr(strtolower($header_key), 0, 6) === 'x-amz-')
        {
            $string_to_sign .= strtolower($header_key) . ':' . $header_value . "\n";
        }
    }

    // Add the signable resource location
    $string_to_sign .= ($this->resource_prefix ? $this->resource_prefix : '');
    $string_to_sign .= (($bucket === '' || $this->resource_prefix === '/' . $bucket) ? '' : ('/' . $bucket)) . $signable_resource . urldecode($signable_query_string);

    // Hash the AWS secret key and generate a signature for the request.
    $signature = base64_encode(hash_hmac('sha1', $string_to_sign, $this->secret_key, true));
    $request->add_header('Authorization', 'AWS ' . $this->key . ':' . $signature);

    // If we're generating a URL, return the URL to the calling method.
    if (isset($opt['preauth']) && (integer) $opt['preauth'] > 0)
    {
        $query_params = array(
            'AWSAccessKeyId' => $this->key,
            'Expires' => $headers['Expires'],
            'Signature' => $signature,
        );

        // If using short-term credentials, add the token to the query string
        if ($this->auth_token)
        {
            $query_params['x-amz-security-token'] = $this->auth_token;
        }

        return $this->request_url . $conjunction . http_build_query($query_params, '', '&');
    }
    elseif (isset($opt['preauth']))
    {
        return $this->request_url;
    }

    /*%******************************************************************************************%*/

    // If our changes were temporary, reset them.
    if ($this->temporary_prefix)
    {
        $this->temporary_prefix = false;
        $this->resource_prefix = null;
    }

    // Manage the (newer) batch request API or the (older) returnCurlHandle setting.
    if ($this->use_batch_flow)
    {
        $handle = $request->prep_request();
        $this->batch_object->add($handle);
        $this->use_batch_flow = false;

        return $handle;
    }
    elseif (isset($opt['returnCurlHandle']) && $opt['returnCurlHandle'] === true)
    {
        return $request->prep_request();
    }

    // Send!
    $request->send_request();

    // Prepare the response
    $headers = $request->get_response_header();
    $headers['x-aws-request-url'] = $this->request_url;
    $headers['x-aws-redirects'] = $this->redirects;
    $headers['x-aws-stringtosign'] = $string_to_sign;
    $headers['x-aws-requestheaders'] = $request->request_headers;

    // Did we have a request body?
    if (isset($opt['body']))
    {
        $headers['x-aws-requestbody'] = $opt['body'];
    }

    $data = new $this->response_class($headers, $this->parse_callback($request->get_response_body()), $request->get_response_code());

    // Did Amazon tell us to redirect? Typically happens for multiple rapid requests EU datacenters.
    // @see: http://docs.amazonwebservices.com/AmazonS3/latest/dev/Redirects.html
    // @codeCoverageIgnoreStart
    if ((integer) $request->get_response_code() === 307) // Temporary redirect to new endpoint.
    {
        $this->redirects++;
        $opt['location'] = $headers['location'];
        $data = $this->authenticate($bucket, $opt);
    }

    // Was it Amazon's fault the request failed? Retry the request until we reach $max_retries.
    elseif ((integer) $request->get_response_code() === 500 || (integer) $request->get_response_code() === 503)
    {
        if ($this->redirects <= $this->max_retries)
        {
            // Exponential backoff
            $delay = (integer) (pow(4, $this->redirects) * 100000);
            usleep($delay);
            $this->redirects++;
            $data = $this->authenticate($bucket, $opt);
        }
    }
    // @codeCoverageIgnoreEnd

    // Return!
    $this->redirects = 0;
    return $data;
}

Copyright © 2010–2013 Amazon Web Services, LLC


Feedback