__accessKey; } public function getSecretKey() { return $this->__secretKey; } public function getHost() { return $this->__host; } protected $__verifyHost = 1; protected $__verifyPeer = 1; // verifyHost and verifyPeer determine whether curl verifies ssl certificates. // It may be necessary to disable these checks on certain systems. // These only have an effect if SSL is enabled. public function verifyHost() { return $this->__verifyHost; } public function enableVerifyHost($enable = true) { $this->__verifyHost = $enable; } public function verifyPeer() { return $this->__verifyPeer; } public function enableVerifyPeer($enable = true) { $this->__verifyPeer = $enable; } /** * Constructor * * @param string $accessKey Access key * @param string $secretKey Secret key * @return void */ public function __construct($accessKey = null, $secretKey = null, $host = 'route53.amazonaws.com') { if ($accessKey !== null && $secretKey !== null) { $this->setAuth($accessKey, $secretKey); } $this->__host = $host; } /** * Set AWS access key and secret key * * @param string $accessKey Access key * @param string $secretKey Secret key * @return void */ public function setAuth($accessKey, $secretKey) { $this->__accessKey = $accessKey; $this->__secretKey = $secretKey; } /** * Lists the hosted zones on the account * * @param string marker A pagination marker returned by a previous truncated call * @param int maxItems The maximum number of items per page. The service uses min($maxItems, 100). * @return A list of hosted zones */ public function listHostedZones($marker = null, $maxItems = 100) { $rest = new Route53Request($this, 'hostedzone', 'GET'); if($marker !== null) { $rest->setParameter('marker', $marker); } if($maxItems !== 100) { $rest->setParameter('maxitems', $maxItems); } $rest = $rest->getResponse(); if($rest->error === false && $rest->code !== 200) { $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); } if($rest->error !== false) { $this->__triggerError('listHostedZones', $rest->error); return false; } $response = array(); if (!isset($rest->body)) { return $response; } $zones = array(); foreach($rest->body->HostedZones->HostedZone as $z) { $zones[] = $this->parseHostedZone($z); } $response['HostedZone'] = $zones; if(isset($rest->body->MaxItems)) { $response['MaxItems'] = (string)$rest->body->MaxItems; } if(isset($rest->body->IsTruncated)) { $response['IsTruncated'] = (string)$rest->body->IsTruncated; if($response['IsTruncated'] == 'true') { $response['NextMarker'] = (string)$rest->body->NextMarker; } } return $response; } /** * Retrieves information on a specified hosted zone * * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', * then that full value should be passed here, including the '/hostedzone/' prefix. * @return A data structure containing information about the specified zone */ public function getHostedZone($zoneId) { // we'll strip off the leading forward slash, so we can use it as the action directly. $zoneId = trim($zoneId, '/'); $rest = new Route53Request($this, $zoneId, 'GET'); $rest = $rest->getResponse(); if($rest->error === false && $rest->code !== 200) { $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); } if($rest->error !== false) { $this->__triggerError('getHostedZone', $rest->error); return false; } $response = array(); if (!isset($rest->body)) { return $response; } $response['HostedZone'] = $this->parseHostedZone($rest->body->HostedZone); $response['NameServers'] = $this->parseDelegationSet($rest->body->DelegationSet); return $response; } /** * Creates a new hosted zone * * @param string name The name of the hosted zone (e.g. "example.com.") * @param string reference A user-specified unique reference for this request * @param string comment An optional user-specified comment to attach to the zone * @return A data structure containing information about the newly created zone */ public function createHostedZone($name, $reference, $comment = '') { // hosted zone names must end with a period, but people will forget this a lot... if(strrpos($name, '.') != (strlen($name) - 1)) { $name .= '.'; } $data = "\n"; $data .= '\n"; $data .= ''.$name."\n"; $data .= ''.$reference."\n"; if(strlen($comment) > 0) { $data .= "\n"; $data .= ''.$comment."\n"; $data .= "\n"; } $data .= "\n"; $rest = new Route53Request($this, 'hostedzone', 'POST', $data); $rest = $rest->getResponse(); if($rest->error === false && !in_array($rest->code, array(200, 201, 202, 204)) ) { $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); } if($rest->error !== false) { $this->__triggerError('createHostedZone', $rest->error); return false; } $response = array(); if (!isset($rest->body)) { return $response; } $response['HostedZone'] = $this->parseHostedZone($rest->body->HostedZone); $response['ChangeInfo'] = $this->parseChangeInfo($rest->body->ChangeInfo); $response['NameServers'] = $this->parseDelegationSet($rest->body->DelegationSet); return $response; } /** * Retrieves information on a specified hosted zone * * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', * then that full value should be passed here, including the '/hostedzone/' prefix. * @return The change request data corresponding to this delete */ public function deleteHostedZone($zoneId) { // we'll strip off the leading forward slash, so we can use it as the action directly. $zoneId = trim($zoneId, '/'); $rest = new Route53Request($this, $zoneId, 'DELETE'); $rest = $rest->getResponse(); if($rest->error === false && $rest->code !== 200) { $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); } if($rest->error !== false) { $this->__triggerError('deleteHostedZone', $rest->error); return false; } if (!isset($rest->body)) { return array(); } return $this->parseChangeInfo($rest->body->ChangeInfo); } /** * Retrieves a list of resource record sets for a given zone * * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', * then that full value should be passed here, including the '/hostedzone/' prefix. * @param string type The type of resource record set to begin listing from. If this is specified, $name must also be specified. * Must be one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SPF, SRV, TXT * @param string name The name at which to begin listing resource records (in the lexographic order of records). * @param int maxItems The maximum number of results to return. The service uses min($maxItems, 100). * @return The list of matching resource record sets */ public function listResourceRecordSets($zoneId, $type = '', $name = '', $maxItems = 100) { // we'll strip off the leading forward slash, so we can use it as the action directly. $zoneId = trim($zoneId, '/'); $rest = new Route53Request($this, $zoneId.'/rrset', 'GET'); if(strlen($type) > 0) { $rest->setParameter('type', $type); } if(strlen($name) > 0) { $rest->setParameter('name', $name); } if($maxItems != 100) { $rest->setParameter('maxitems', $maxItems); } $rest = $rest->getResponse(); if($rest->error === false && $rest->code !== 200) { $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); } if($rest->error !== false) { $this->__triggerError('listResourceRecordSets', $rest->error); return false; } $response = array(); if (!isset($rest->body)) { return $response; } $recordSets = array(); foreach($rest->body->ResourceRecordSets->ResourceRecordSet as $set) { $recordSets[] = $this->parseResourceRecordSet($set); } $response['ResourceRecordSets'] = $recordSets; if(isset($rest->body->MaxItems)) { $response['MaxItems'] = (string)$rest->body->MaxItems; } if(isset($rest->body->IsTruncated)) { $response['IsTruncated'] = (string)$rest->body->IsTruncated; if($response['IsTruncated'] == 'true') { $response['NextRecordName'] = (string)$rest->body->NextRecordName; $response['NextRecordType'] = (string)$rest->body->NextRecordType; } } return $response; } /** * Makes the specified resource record set changes (create or delete). * * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', * then that full value should be passed here, including the '/hostedzone/' prefix. * @param array changes An array of change objects, as they are returned by the prepareChange utility method. * You may also pass a single change object. * @param string comment An optional comment to attach to the change request * @return The status of the change request */ public function changeResourceRecordSets($zoneId, $changes, $comment = '') { // we'll strip off the leading forward slash, so we can use it as the action directly. $zoneId = trim($zoneId, '/'); $data = "\n"; $data .= '\n"; $data .= "\n"; if(strlen($comment) > 0) { $data .= ''.$comment."\n"; } if(!is_array($changes)) { $changes = array($changes); } $data .= "\n"; foreach($changes as $change) { $data .= $change; } $data .= "\n"; $data .= "\n"; $data .= "\n"; $rest = new Route53Request($this, $zoneId.'/rrset', 'POST', $data); $rest = $rest->getResponse(); if($rest->error === false && !in_array($rest->code, array(200, 201, 202, 204))) { $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); } if($rest->error !== false) { $this->__triggerError('changeResourceRecordSets', $rest->error); return false; } if (!isset($rest->body)) { return array(); } return $this->parseChangeInfo($rest->body->ChangeInfo); } /** * Retrieves information on a specified change request * * @param string changeId The id of the change, as returned by CreateHostedZoneResponse or ChangeResourceRecordSets * In other words, if CreateHostedZoneResponse showed the change's Id as '/change/C2682N5HXP0BZ4', * then that full value should be passed here, including the '/change/' prefix. * @return The status of the change request */ public function getChange($changeId) { // we'll strip off the leading forward slash, so we can use it as the action directly. $zoneId = trim($changeId, '/'); $rest = new Route53Request($this, $changeId, 'GET'); $rest = $rest->getResponse(); if($rest->error === false && $rest->code !== 200) { $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); } if($rest->error !== false) { $this->__triggerError('getChange', $rest->error); return false; } if (!isset($rest->body)) { return array(); } return $this->parseChangeInfo($rest->body->ChangeInfo); } /** * Utility function to parse a HostedZone tag structure */ private function parseHostedZone($tag) { $zone = array(); $zone['Id'] = (string)$tag->Id; $zone['Name'] = (string)$tag->Name; $zone['CallerReference'] = (string)$tag->CallerReference; // these might always be set, but check just in case, since // their values are option on CreateHostedZone requests if(isset($tag->Config) && isset($tag->Config->Comment)) { $zone['Config'] = array('Comment' => (string)$tag->Config->Comment); } return $zone; } /** * Utility function to parse a ChangeInfo tag structure */ private function parseChangeInfo($tag) { $info = array(); $info['Id'] = (string)$tag->Id; $info['Status'] = (string)$tag->Status; $info['SubmittedAt'] = (string)$tag->SubmittedAt; return $info; } /** * Utility function to parse a DelegationSet tag structure */ private function parseDelegationSet($tag) { $servers = array(); foreach($tag->NameServers->NameServer as $ns) { $servers[] = (string)$ns; } return $servers; } /** * Utility function to parse a ResourceRecordSet tag structure */ private function parseResourceRecordSet($tag) { $rrs = array(); $rrs['Name'] = (string)$tag->Name; $rrs['Type'] = (string)$tag->Type; $rrs['TTL'] = (string)$tag->TTL; $rrs['ResourceRecords'] = array(); foreach($tag->ResourceRecords->ResourceRecord as $rr) { $rrs['ResourceRecords'][] = (string)$rr->Value; } return $rrs; } /** * Utility function to prepare a Change object for ChangeResourceRecordSets requests. * All fields are required. * * @param string action The action to perform. One of: CREATE, DELETE * @param string name The name to perform the action on. * If it does not end with '.', then AWS treats the name as relative to the zone root. * @param string type The type of record being modified. * Must be one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SPF, SRV, TXT * @param int ttl The time-to-live value for this record, in seconds. * @param array records An array of resource records to attach to this change. * Each member of this array can either be a string, or an array of strings. * Passing an array of strings will attach multiple values to a single resource record. * If a single string is passed as $records instead of an array, * it will be treated as a single-member array. * @return object An opaque object containing the change request. * Do not write code that depends on the contents of this object, as it may change at any time. */ public function prepareChange($action, $name, $type, $ttl, $records) { $change = "\n"; $change .= ''.$action."\n"; $change .= "\n"; $change .= ''.$name."\n"; $change .= ''.$type."\n"; $change .= ''.$ttl."\n"; $change .= "\n"; if(!is_array($records)) { $records = array($records); } foreach($records as $record) { $change .= "\n"; if(is_array($record)) { foreach($record as $value) { $change .= ''.$value."\n"; } } else { $change .= ''.$record."\n"; } $change .= "\n"; } $change .= "\n"; $change .= "\n"; $change .= "\n"; return $change; } /** * Trigger an error message * * @internal Used by member functions to output errors * @param array $error Array containing error information * @return string */ public function __triggerError($functionname, $error) { if($error == false) { trigger_error(sprintf("Route53::%s(): Encountered an error, but no description given", $functionname), E_USER_WARNING); } else if(isset($error['curl']) && $error['curl']) { trigger_error(sprintf("Route53::%s(): %s %s", $functionname, $error['code'], $error['message']), E_USER_WARNING); } else if(isset($error['Error'])) { $e = $error['Error']; $message = sprintf("Route53::%s(): %s - %s: %s\nRequest Id: %s\n", $functionname, $e['Type'], $e['Code'], $e['Message'], $error['RequestId']); trigger_error($message, E_USER_WARNING); } } /** * Callback handler for 503 retries. * * @internal Used by SimpleDBRequest to call the user-specified callback, if set * @param $attempt The number of failed attempts so far * @return The retry delay in microseconds, or 0 to stop retrying. */ public function __executeServiceTemporarilyUnavailableRetryDelay($attempt) { if(is_callable($this->__serviceUnavailableRetryDelayCallback)) { $callback = $this->__serviceUnavailableRetryDelayCallback; return $callback($attempt); } return 0; } } final class Route53Request { private $r53, $action, $verb, $data, $parameters = array(); public $response; /** * Constructor * * @param string $r53 The Route53 object making this request * @param string $action SimpleDB action * @param string $verb HTTP verb * @param string $data For POST requests, the data being posted (optional) * @return mixed */ function __construct($r53, $action, $verb, $data = '') { $this->r53 = $r53; $this->action = $action; $this->verb = $verb; $this->data = $data; $this->response = new STDClass; $this->response->error = false; } /** * Set request parameter * * @param string $key Key * @param string $value Value * @param boolean $replace Whether to replace the key if it already exists (default true) * @return void */ public function setParameter($key, $value, $replace = true) { if(!$replace && isset($this->parameters[$key])) { $temp = (array)($this->parameters[$key]); $temp[] = $value; $this->parameters[$key] = $temp; } else { $this->parameters[$key] = $value; } } /** * Get the response * * @return object | false */ public function getResponse() { $params = array(); foreach ($this->parameters as $var => $value) { if(is_array($value)) { foreach($value as $v) { $params[] = $var.'='.$this->__customUrlEncode($v); } } else { $params[] = $var.'='.$this->__customUrlEncode($value); } } sort($params, SORT_STRING); $query = implode('&', $params); // must be in format 'Sun, 06 Nov 1994 08:49:37 GMT' $date = gmdate('D, d M Y H:i:s e'); $headers = array(); $headers[] = 'Date: '.$date; $headers[] = 'Host: '.$this->r53->getHost(); $auth = 'AWS3-HTTPS AWSAccessKeyId='.$this->r53->getAccessKey(); $auth .= ',Algorithm=HmacSHA256,Signature='.$this->__getSignature($date); $headers[] = 'X-Amzn-Authorization: '.$auth; $url = 'https://'.$this->r53->getHost().'/'.Route53::API_VERSION.'/'.$this->action.'?'.$query; // Basic setup $curl = curl_init(); curl_setopt($curl, CURLOPT_USERAGENT, 'Route53/php'); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, ($this->r53->verifyHost() ? 1 : 0)); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, ($this->r53->verifyPeer() ? 1 : 0)); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); // Request types switch ($this->verb) { case 'GET': break; case 'POST': curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); if(strlen($this->data) > 0) { curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); $headers[] = 'Content-Type: text/plain'; $headers[] = 'Content-Length: '.strlen($this->data); } break; case 'DELETE': curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); break; default: break; } curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_HEADER, false); // Execute, grab errors if (curl_exec($curl)) { $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); } else { $this->response->error = array( 'curl' => true, 'code' => curl_errno($curl), 'message' => curl_error($curl), 'resource' => $this->resource ); } @curl_close($curl); // Parse body into XML if ($this->response->error === false && isset($this->response->body)) { $this->response->body = simplexml_load_string($this->response->body); // Grab Route53 errors if (!in_array($this->response->code, array(200, 201, 202, 204)) && isset($this->response->body->Error)) { $error = $this->response->body->Error; $output = array(); $output['curl'] = false; $output['Error'] = array(); $output['Error']['Type'] = (string)$error->Type; $output['Error']['Code'] = (string)$error->Code; $output['Error']['Message'] = (string)$error->Message; $output['RequestId'] = (string)$this->response->body->RequestId; $this->response->error = $output; unset($this->response->body); } } return $this->response; } /** * CURL write callback * * @param resource &$curl CURL resource * @param string &$data Data * @return integer */ private function __responseWriteCallback(&$curl, &$data) { $this->response->body .= $data; return strlen($data); } /** * Contributed by afx114 * URL encode the parameters as per http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?Query_QueryAuth.html * PHP's rawurlencode() follows RFC 1738, not RFC 3986 as required by Amazon. The only difference is the tilde (~), so convert it back after rawurlencode * See: http://www.morganney.com/blog/API/AWS-Product-Advertising-API-Requires-a-Signed-Request.php * * @param string $var String to encode * @return string */ private function __customUrlEncode($var) { return str_replace('%7E', '~', rawurlencode($var)); } /** * Generate the auth string using Hmac-SHA256 * * @internal Used by SimpleDBRequest::getResponse() * @param string $string String to sign * @return string */ private function __getSignature($string) { return base64_encode(hash_hmac('sha256', $string, $this->r53->getSecretKey(), true)); } }