1
|
<?php
|
2
|
/**
|
3
|
*
|
4
|
* Copyright (c) 2011, Dan Myers.
|
5
|
* Parts copyright (c) 2008, Donovan Schonknecht.
|
6
|
* All rights reserved.
|
7
|
*
|
8
|
* Redistribution and use in source and binary forms, with or without
|
9
|
* modification, are permitted provided that the following conditions are met:
|
10
|
*
|
11
|
* - Redistributions of source code must retain the above copyright notice,
|
12
|
* this list of conditions and the following disclaimer.
|
13
|
* - Redistributions in binary form must reproduce the above copyright
|
14
|
* notice, this list of conditions and the following disclaimer in the
|
15
|
* documentation and/or other materials provided with the distribution.
|
16
|
*
|
17
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
21
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
22
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
23
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
25
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
26
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27
|
* POSSIBILITY OF SUCH DAMAGE.
|
28
|
*
|
29
|
* This is a modified BSD license (the third clause has been removed).
|
30
|
* The BSD license may be found here:
|
31
|
* http://www.opensource.org/licenses/bsd-license.php
|
32
|
*
|
33
|
* Amazon Route 53 is a trademark of Amazon.com, Inc. or its affiliates.
|
34
|
*
|
35
|
* Route53 is based on Donovan Schonknecht's Amazon S3 PHP class, found here:
|
36
|
* http://undesigned.org.za/2007/10/22/amazon-s3-php-class
|
37
|
*
|
38
|
*/
|
39
|
|
40
|
/**
|
41
|
* Amazon Route53 PHP class
|
42
|
*
|
43
|
* @link http://sourceforge.net/projects/php-r53/
|
44
|
* version 0.9.0
|
45
|
*
|
46
|
*/
|
47
|
class Route53
|
48
|
{
|
49
|
const API_VERSION = '2010-10-01';
|
50
|
|
51
|
protected $__accessKey; // AWS Access key
|
52
|
protected $__secretKey; // AWS Secret key
|
53
|
protected $__host;
|
54
|
|
55
|
public function getAccessKey() { return $this->__accessKey; }
|
56
|
public function getSecretKey() { return $this->__secretKey; }
|
57
|
public function getHost() { return $this->__host; }
|
58
|
|
59
|
protected $__verifyHost = 1;
|
60
|
protected $__verifyPeer = 1;
|
61
|
|
62
|
// verifyHost and verifyPeer determine whether curl verifies ssl certificates.
|
63
|
// It may be necessary to disable these checks on certain systems.
|
64
|
// These only have an effect if SSL is enabled.
|
65
|
public function verifyHost() { return $this->__verifyHost; }
|
66
|
public function enableVerifyHost($enable = true) { $this->__verifyHost = $enable; }
|
67
|
|
68
|
public function verifyPeer() { return $this->__verifyPeer; }
|
69
|
public function enableVerifyPeer($enable = true) { $this->__verifyPeer = $enable; }
|
70
|
|
71
|
/**
|
72
|
* Constructor
|
73
|
*
|
74
|
* @param string $accessKey Access key
|
75
|
* @param string $secretKey Secret key
|
76
|
* @return void
|
77
|
*/
|
78
|
public function __construct($accessKey = null, $secretKey = null, $host = 'route53.amazonaws.com') {
|
79
|
if ($accessKey !== null && $secretKey !== null) {
|
80
|
$this->setAuth($accessKey, $secretKey);
|
81
|
}
|
82
|
$this->__host = $host;
|
83
|
}
|
84
|
|
85
|
/**
|
86
|
* Set AWS access key and secret key
|
87
|
*
|
88
|
* @param string $accessKey Access key
|
89
|
* @param string $secretKey Secret key
|
90
|
* @return void
|
91
|
*/
|
92
|
public function setAuth($accessKey, $secretKey) {
|
93
|
$this->__accessKey = $accessKey;
|
94
|
$this->__secretKey = $secretKey;
|
95
|
}
|
96
|
|
97
|
/**
|
98
|
* Lists the hosted zones on the account
|
99
|
*
|
100
|
* @param string marker A pagination marker returned by a previous truncated call
|
101
|
* @param int maxItems The maximum number of items per page. The service uses min($maxItems, 100).
|
102
|
* @return A list of hosted zones
|
103
|
*/
|
104
|
public function listHostedZones($marker = null, $maxItems = 100) {
|
105
|
$rest = new Route53Request($this, 'hostedzone', 'GET');
|
106
|
|
107
|
if($marker !== null) {
|
108
|
$rest->setParameter('marker', $marker);
|
109
|
}
|
110
|
if($maxItems !== 100) {
|
111
|
$rest->setParameter('maxitems', $maxItems);
|
112
|
}
|
113
|
|
114
|
$rest = $rest->getResponse();
|
115
|
if($rest->error === false && $rest->code !== 200) {
|
116
|
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
|
117
|
}
|
118
|
if($rest->error !== false) {
|
119
|
$this->__triggerError('listHostedZones', $rest->error);
|
120
|
return false;
|
121
|
}
|
122
|
|
123
|
$response = array();
|
124
|
if (!isset($rest->body))
|
125
|
{
|
126
|
return $response;
|
127
|
}
|
128
|
|
129
|
$zones = array();
|
130
|
foreach($rest->body->HostedZones->HostedZone as $z)
|
131
|
{
|
132
|
$zones[] = $this->parseHostedZone($z);
|
133
|
}
|
134
|
$response['HostedZone'] = $zones;
|
135
|
|
136
|
if(isset($rest->body->MaxItems)) {
|
137
|
$response['MaxItems'] = (string)$rest->body->MaxItems;
|
138
|
}
|
139
|
|
140
|
if(isset($rest->body->IsTruncated)) {
|
141
|
$response['IsTruncated'] = (string)$rest->body->IsTruncated;
|
142
|
if($response['IsTruncated'] == 'true') {
|
143
|
$response['NextMarker'] = (string)$rest->body->NextMarker;
|
144
|
}
|
145
|
}
|
146
|
|
147
|
return $response;
|
148
|
}
|
149
|
|
150
|
/**
|
151
|
* Retrieves information on a specified hosted zone
|
152
|
*
|
153
|
* @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse
|
154
|
* In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9',
|
155
|
* then that full value should be passed here, including the '/hostedzone/' prefix.
|
156
|
* @return A data structure containing information about the specified zone
|
157
|
*/
|
158
|
public function getHostedZone($zoneId) {
|
159
|
// we'll strip off the leading forward slash, so we can use it as the action directly.
|
160
|
$zoneId = trim($zoneId, '/');
|
161
|
|
162
|
$rest = new Route53Request($this, $zoneId, 'GET');
|
163
|
|
164
|
$rest = $rest->getResponse();
|
165
|
if($rest->error === false && $rest->code !== 200) {
|
166
|
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
|
167
|
}
|
168
|
if($rest->error !== false) {
|
169
|
$this->__triggerError('getHostedZone', $rest->error);
|
170
|
return false;
|
171
|
}
|
172
|
|
173
|
$response = array();
|
174
|
if (!isset($rest->body))
|
175
|
{
|
176
|
return $response;
|
177
|
}
|
178
|
|
179
|
$response['HostedZone'] = $this->parseHostedZone($rest->body->HostedZone);
|
180
|
$response['NameServers'] = $this->parseDelegationSet($rest->body->DelegationSet);
|
181
|
|
182
|
return $response;
|
183
|
}
|
184
|
|
185
|
/**
|
186
|
* Creates a new hosted zone
|
187
|
*
|
188
|
* @param string name The name of the hosted zone (e.g. "example.com.")
|
189
|
* @param string reference A user-specified unique reference for this request
|
190
|
* @param string comment An optional user-specified comment to attach to the zone
|
191
|
* @return A data structure containing information about the newly created zone
|
192
|
*/
|
193
|
public function createHostedZone($name, $reference, $comment = '') {
|
194
|
// hosted zone names must end with a period, but people will forget this a lot...
|
195
|
if(strrpos($name, '.') != (strlen($name) - 1)) {
|
196
|
$name .= '.';
|
197
|
}
|
198
|
|
199
|
$data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
200
|
$data .= '<CreateHostedZoneRequest xmlns="https://route53.amazonaws.com/doc/'.Route53::API_VERSION."/\">\n";
|
201
|
$data .= '<Name>'.$name."</Name>\n";
|
202
|
$data .= '<CallerReference>'.$reference."</CallerReference>\n";
|
203
|
if(strlen($comment) > 0) {
|
204
|
$data .= "<HostedZoneConfig>\n";
|
205
|
$data .= '<Comment>'.$comment."</Comment>\n";
|
206
|
$data .= "</HostedZoneConfig>\n";
|
207
|
}
|
208
|
$data .= "</CreateHostedZoneRequest>\n";
|
209
|
|
210
|
$rest = new Route53Request($this, 'hostedzone', 'POST', $data);
|
211
|
|
212
|
$rest = $rest->getResponse();
|
213
|
|
214
|
if($rest->error === false && !in_array($rest->code, array(200, 201, 202, 204)) ) {
|
215
|
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
|
216
|
}
|
217
|
if($rest->error !== false) {
|
218
|
$this->__triggerError('createHostedZone', $rest->error);
|
219
|
return false;
|
220
|
}
|
221
|
|
222
|
$response = array();
|
223
|
if (!isset($rest->body))
|
224
|
{
|
225
|
return $response;
|
226
|
}
|
227
|
|
228
|
$response['HostedZone'] = $this->parseHostedZone($rest->body->HostedZone);
|
229
|
$response['ChangeInfo'] = $this->parseChangeInfo($rest->body->ChangeInfo);
|
230
|
$response['NameServers'] = $this->parseDelegationSet($rest->body->DelegationSet);
|
231
|
|
232
|
return $response;
|
233
|
}
|
234
|
|
235
|
/**
|
236
|
* Retrieves information on a specified hosted zone
|
237
|
*
|
238
|
* @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse
|
239
|
* In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9',
|
240
|
* then that full value should be passed here, including the '/hostedzone/' prefix.
|
241
|
* @return The change request data corresponding to this delete
|
242
|
*/
|
243
|
public function deleteHostedZone($zoneId) {
|
244
|
// we'll strip off the leading forward slash, so we can use it as the action directly.
|
245
|
$zoneId = trim($zoneId, '/');
|
246
|
|
247
|
$rest = new Route53Request($this, $zoneId, 'DELETE');
|
248
|
|
249
|
$rest = $rest->getResponse();
|
250
|
if($rest->error === false && $rest->code !== 200) {
|
251
|
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
|
252
|
}
|
253
|
if($rest->error !== false) {
|
254
|
$this->__triggerError('deleteHostedZone', $rest->error);
|
255
|
return false;
|
256
|
}
|
257
|
|
258
|
if (!isset($rest->body))
|
259
|
{
|
260
|
return array();
|
261
|
}
|
262
|
|
263
|
return $this->parseChangeInfo($rest->body->ChangeInfo);
|
264
|
}
|
265
|
|
266
|
/**
|
267
|
* Retrieves a list of resource record sets for a given zone
|
268
|
*
|
269
|
* @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse
|
270
|
* In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9',
|
271
|
* then that full value should be passed here, including the '/hostedzone/' prefix.
|
272
|
* @param string type The type of resource record set to begin listing from. If this is specified, $name must also be specified.
|
273
|
* Must be one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SPF, SRV, TXT
|
274
|
* @param string name The name at which to begin listing resource records (in the lexographic order of records).
|
275
|
* @param int maxItems The maximum number of results to return. The service uses min($maxItems, 100).
|
276
|
* @return The list of matching resource record sets
|
277
|
*/
|
278
|
public function listResourceRecordSets($zoneId, $type = '', $name = '', $maxItems = 100) {
|
279
|
// we'll strip off the leading forward slash, so we can use it as the action directly.
|
280
|
$zoneId = trim($zoneId, '/');
|
281
|
|
282
|
$rest = new Route53Request($this, $zoneId.'/rrset', 'GET');
|
283
|
|
284
|
if(strlen($type) > 0) {
|
285
|
$rest->setParameter('type', $type);
|
286
|
}
|
287
|
if(strlen($name) > 0) {
|
288
|
$rest->setParameter('name', $name);
|
289
|
}
|
290
|
if($maxItems != 100) {
|
291
|
$rest->setParameter('maxitems', $maxItems);
|
292
|
}
|
293
|
|
294
|
$rest = $rest->getResponse();
|
295
|
if($rest->error === false && $rest->code !== 200) {
|
296
|
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
|
297
|
}
|
298
|
if($rest->error !== false) {
|
299
|
$this->__triggerError('listResourceRecordSets', $rest->error);
|
300
|
return false;
|
301
|
}
|
302
|
|
303
|
$response = array();
|
304
|
if (!isset($rest->body))
|
305
|
{
|
306
|
return $response;
|
307
|
}
|
308
|
|
309
|
$recordSets = array();
|
310
|
foreach($rest->body->ResourceRecordSets->ResourceRecordSet as $set) {
|
311
|
$recordSets[] = $this->parseResourceRecordSet($set);
|
312
|
}
|
313
|
|
314
|
$response['ResourceRecordSets'] = $recordSets;
|
315
|
|
316
|
if(isset($rest->body->MaxItems)) {
|
317
|
$response['MaxItems'] = (string)$rest->body->MaxItems;
|
318
|
}
|
319
|
|
320
|
if(isset($rest->body->IsTruncated)) {
|
321
|
$response['IsTruncated'] = (string)$rest->body->IsTruncated;
|
322
|
if($response['IsTruncated'] == 'true') {
|
323
|
$response['NextRecordName'] = (string)$rest->body->NextRecordName;
|
324
|
$response['NextRecordType'] = (string)$rest->body->NextRecordType;
|
325
|
}
|
326
|
}
|
327
|
|
328
|
return $response;
|
329
|
}
|
330
|
|
331
|
/**
|
332
|
* Makes the specified resource record set changes (create or delete).
|
333
|
*
|
334
|
* @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse
|
335
|
* In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9',
|
336
|
* then that full value should be passed here, including the '/hostedzone/' prefix.
|
337
|
* @param array changes An array of change objects, as they are returned by the prepareChange utility method.
|
338
|
* You may also pass a single change object.
|
339
|
* @param string comment An optional comment to attach to the change request
|
340
|
* @return The status of the change request
|
341
|
*/
|
342
|
public function changeResourceRecordSets($zoneId, $changes, $comment = '') {
|
343
|
// we'll strip off the leading forward slash, so we can use it as the action directly.
|
344
|
$zoneId = trim($zoneId, '/');
|
345
|
|
346
|
$data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
347
|
$data .= '<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/'.Route53::API_VERSION."/\">\n";
|
348
|
$data .= "<ChangeBatch>\n";
|
349
|
|
350
|
if(strlen($comment) > 0) {
|
351
|
$data .= '<Comment>'.$comment."</Comment>\n";
|
352
|
}
|
353
|
|
354
|
if(!is_array($changes)) {
|
355
|
$changes = array($changes);
|
356
|
}
|
357
|
|
358
|
$data .= "<Changes>\n";
|
359
|
foreach($changes as $change) {
|
360
|
$data .= $change;
|
361
|
}
|
362
|
$data .= "</Changes>\n";
|
363
|
|
364
|
$data .= "</ChangeBatch>\n";
|
365
|
$data .= "</ChangeResourceRecordSetsRequest>\n";
|
366
|
|
367
|
$rest = new Route53Request($this, $zoneId.'/rrset', 'POST', $data);
|
368
|
|
369
|
$rest = $rest->getResponse();
|
370
|
if($rest->error === false && !in_array($rest->code, array(200, 201, 202, 204))) {
|
371
|
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
|
372
|
}
|
373
|
if($rest->error !== false) {
|
374
|
$this->__triggerError('changeResourceRecordSets', $rest->error);
|
375
|
return false;
|
376
|
}
|
377
|
|
378
|
if (!isset($rest->body))
|
379
|
{
|
380
|
return array();
|
381
|
}
|
382
|
|
383
|
return $this->parseChangeInfo($rest->body->ChangeInfo);
|
384
|
}
|
385
|
|
386
|
/**
|
387
|
* Retrieves information on a specified change request
|
388
|
*
|
389
|
* @param string changeId The id of the change, as returned by CreateHostedZoneResponse or ChangeResourceRecordSets
|
390
|
* In other words, if CreateHostedZoneResponse showed the change's Id as '/change/C2682N5HXP0BZ4',
|
391
|
* then that full value should be passed here, including the '/change/' prefix.
|
392
|
* @return The status of the change request
|
393
|
*/
|
394
|
public function getChange($changeId) {
|
395
|
// we'll strip off the leading forward slash, so we can use it as the action directly.
|
396
|
$zoneId = trim($changeId, '/');
|
397
|
|
398
|
$rest = new Route53Request($this, $changeId, 'GET');
|
399
|
|
400
|
$rest = $rest->getResponse();
|
401
|
if($rest->error === false && $rest->code !== 200) {
|
402
|
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
|
403
|
}
|
404
|
if($rest->error !== false) {
|
405
|
$this->__triggerError('getChange', $rest->error);
|
406
|
return false;
|
407
|
}
|
408
|
|
409
|
if (!isset($rest->body))
|
410
|
{
|
411
|
return array();
|
412
|
}
|
413
|
|
414
|
return $this->parseChangeInfo($rest->body->ChangeInfo);
|
415
|
}
|
416
|
|
417
|
|
418
|
|
419
|
/**
|
420
|
* Utility function to parse a HostedZone tag structure
|
421
|
*/
|
422
|
private function parseHostedZone($tag) {
|
423
|
$zone = array();
|
424
|
$zone['Id'] = (string)$tag->Id;
|
425
|
$zone['Name'] = (string)$tag->Name;
|
426
|
$zone['CallerReference'] = (string)$tag->CallerReference;
|
427
|
|
428
|
// these might always be set, but check just in case, since
|
429
|
// their values are option on CreateHostedZone requests
|
430
|
if(isset($tag->Config) && isset($tag->Config->Comment)) {
|
431
|
$zone['Config'] = array('Comment' => (string)$tag->Config->Comment);
|
432
|
}
|
433
|
|
434
|
return $zone;
|
435
|
}
|
436
|
|
437
|
/**
|
438
|
* Utility function to parse a ChangeInfo tag structure
|
439
|
*/
|
440
|
private function parseChangeInfo($tag) {
|
441
|
$info = array();
|
442
|
$info['Id'] = (string)$tag->Id;
|
443
|
$info['Status'] = (string)$tag->Status;
|
444
|
$info['SubmittedAt'] = (string)$tag->SubmittedAt;
|
445
|
return $info;
|
446
|
}
|
447
|
|
448
|
/**
|
449
|
* Utility function to parse a DelegationSet tag structure
|
450
|
*/
|
451
|
private function parseDelegationSet($tag) {
|
452
|
$servers = array();
|
453
|
foreach($tag->NameServers->NameServer as $ns) {
|
454
|
$servers[] = (string)$ns;
|
455
|
}
|
456
|
return $servers;
|
457
|
}
|
458
|
|
459
|
/**
|
460
|
* Utility function to parse a ResourceRecordSet tag structure
|
461
|
*/
|
462
|
private function parseResourceRecordSet($tag) {
|
463
|
$rrs = array();
|
464
|
$rrs['Name'] = (string)$tag->Name;
|
465
|
$rrs['Type'] = (string)$tag->Type;
|
466
|
$rrs['TTL'] = (string)$tag->TTL;
|
467
|
$rrs['ResourceRecords'] = array();
|
468
|
foreach($tag->ResourceRecords->ResourceRecord as $rr) {
|
469
|
$rrs['ResourceRecords'][] = (string)$rr->Value;
|
470
|
}
|
471
|
return $rrs;
|
472
|
}
|
473
|
|
474
|
/**
|
475
|
* Utility function to prepare a Change object for ChangeResourceRecordSets requests.
|
476
|
* All fields are required.
|
477
|
*
|
478
|
* @param string action The action to perform. One of: CREATE, DELETE
|
479
|
* @param string name The name to perform the action on.
|
480
|
* If it does not end with '.', then AWS treats the name as relative to the zone root.
|
481
|
* @param string type The type of record being modified.
|
482
|
* Must be one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SPF, SRV, TXT
|
483
|
* @param int ttl The time-to-live value for this record, in seconds.
|
484
|
* @param array records An array of resource records to attach to this change.
|
485
|
* Each member of this array can either be a string, or an array of strings.
|
486
|
* Passing an array of strings will attach multiple values to a single resource record.
|
487
|
* If a single string is passed as $records instead of an array,
|
488
|
* it will be treated as a single-member array.
|
489
|
* @return object An opaque object containing the change request.
|
490
|
* Do not write code that depends on the contents of this object, as it may change at any time.
|
491
|
*/
|
492
|
public function prepareChange($action, $name, $type, $ttl, $records) {
|
493
|
$change = "<Change>\n";
|
494
|
$change .= '<Action>'.$action."</Action>\n";
|
495
|
$change .= "<ResourceRecordSet>\n";
|
496
|
$change .= '<Name>'.$name."</Name>\n";
|
497
|
$change .= '<Type>'.$type."</Type>\n";
|
498
|
$change .= '<TTL>'.$ttl."</TTL>\n";
|
499
|
$change .= "<ResourceRecords>\n";
|
500
|
|
501
|
if(!is_array($records)) {
|
502
|
$records = array($records);
|
503
|
}
|
504
|
|
505
|
foreach($records as $record) {
|
506
|
$change .= "<ResourceRecord>\n";
|
507
|
if(is_array($record)) {
|
508
|
foreach($record as $value) {
|
509
|
$change .= '<Value>'.$value."</Value>\n";
|
510
|
}
|
511
|
}
|
512
|
else {
|
513
|
$change .= '<Value>'.$record."</Value>\n";
|
514
|
}
|
515
|
$change .= "</ResourceRecord>\n";
|
516
|
}
|
517
|
|
518
|
$change .= "</ResourceRecords>\n";
|
519
|
$change .= "</ResourceRecordSet>\n";
|
520
|
$change .= "</Change>\n";
|
521
|
|
522
|
return $change;
|
523
|
}
|
524
|
|
525
|
/**
|
526
|
* Trigger an error message
|
527
|
*
|
528
|
* @internal Used by member functions to output errors
|
529
|
* @param array $error Array containing error information
|
530
|
* @return string
|
531
|
*/
|
532
|
public function __triggerError($functionname, $error)
|
533
|
{
|
534
|
if($error == false) {
|
535
|
trigger_error(sprintf("Route53::%s(): Encountered an error, but no description given", $functionname), E_USER_WARNING);
|
536
|
}
|
537
|
else if(isset($error['curl']) && $error['curl'])
|
538
|
{
|
539
|
trigger_error(sprintf("Route53::%s(): %s %s", $functionname, $error['code'], $error['message']), E_USER_WARNING);
|
540
|
}
|
541
|
else if(isset($error['Error']))
|
542
|
{
|
543
|
$e = $error['Error'];
|
544
|
$message = sprintf("Route53::%s(): %s - %s: %s\nRequest Id: %s\n", $functionname, $e['Type'], $e['Code'], $e['Message'], $error['RequestId']);
|
545
|
trigger_error($message, E_USER_WARNING);
|
546
|
}
|
547
|
}
|
548
|
|
549
|
/**
|
550
|
* Callback handler for 503 retries.
|
551
|
*
|
552
|
* @internal Used by SimpleDBRequest to call the user-specified callback, if set
|
553
|
* @param $attempt The number of failed attempts so far
|
554
|
* @return The retry delay in microseconds, or 0 to stop retrying.
|
555
|
*/
|
556
|
public function __executeServiceTemporarilyUnavailableRetryDelay($attempt)
|
557
|
{
|
558
|
if(is_callable($this->__serviceUnavailableRetryDelayCallback)) {
|
559
|
$callback = $this->__serviceUnavailableRetryDelayCallback;
|
560
|
return $callback($attempt);
|
561
|
}
|
562
|
return 0;
|
563
|
}
|
564
|
}
|
565
|
|
566
|
final class Route53Request
|
567
|
{
|
568
|
private $r53, $action, $verb, $data, $parameters = array();
|
569
|
public $response;
|
570
|
|
571
|
/**
|
572
|
* Constructor
|
573
|
*
|
574
|
* @param string $r53 The Route53 object making this request
|
575
|
* @param string $action SimpleDB action
|
576
|
* @param string $verb HTTP verb
|
577
|
* @param string $data For POST requests, the data being posted (optional)
|
578
|
* @return mixed
|
579
|
*/
|
580
|
function __construct($r53, $action, $verb, $data = '') {
|
581
|
$this->r53 = $r53;
|
582
|
$this->action = $action;
|
583
|
$this->verb = $verb;
|
584
|
$this->data = $data;
|
585
|
$this->response = new STDClass;
|
586
|
$this->response->error = false;
|
587
|
}
|
588
|
|
589
|
/**
|
590
|
* Set request parameter
|
591
|
*
|
592
|
* @param string $key Key
|
593
|
* @param string $value Value
|
594
|
* @param boolean $replace Whether to replace the key if it already exists (default true)
|
595
|
* @return void
|
596
|
*/
|
597
|
public function setParameter($key, $value, $replace = true) {
|
598
|
if(!$replace && isset($this->parameters[$key]))
|
599
|
{
|
600
|
$temp = (array)($this->parameters[$key]);
|
601
|
$temp[] = $value;
|
602
|
$this->parameters[$key] = $temp;
|
603
|
}
|
604
|
else
|
605
|
{
|
606
|
$this->parameters[$key] = $value;
|
607
|
}
|
608
|
}
|
609
|
|
610
|
/**
|
611
|
* Get the response
|
612
|
*
|
613
|
* @return object | false
|
614
|
*/
|
615
|
public function getResponse() {
|
616
|
|
617
|
$params = array();
|
618
|
foreach ($this->parameters as $var => $value)
|
619
|
{
|
620
|
if(is_array($value))
|
621
|
{
|
622
|
foreach($value as $v)
|
623
|
{
|
624
|
$params[] = $var.'='.$this->__customUrlEncode($v);
|
625
|
}
|
626
|
}
|
627
|
else
|
628
|
{
|
629
|
$params[] = $var.'='.$this->__customUrlEncode($value);
|
630
|
}
|
631
|
}
|
632
|
|
633
|
sort($params, SORT_STRING);
|
634
|
|
635
|
$query = implode('&', $params);
|
636
|
|
637
|
// must be in format 'Sun, 06 Nov 1994 08:49:37 GMT'
|
638
|
$date = gmdate('D, d M Y H:i:s e');
|
639
|
|
640
|
$headers = array();
|
641
|
$headers[] = 'Date: '.$date;
|
642
|
$headers[] = 'Host: '.$this->r53->getHost();
|
643
|
|
644
|
$auth = 'AWS3-HTTPS AWSAccessKeyId='.$this->r53->getAccessKey();
|
645
|
$auth .= ',Algorithm=HmacSHA256,Signature='.$this->__getSignature($date);
|
646
|
$headers[] = 'X-Amzn-Authorization: '.$auth;
|
647
|
|
648
|
$url = 'https://'.$this->r53->getHost().'/'.Route53::API_VERSION.'/'.$this->action.'?'.$query;
|
649
|
|
650
|
// Basic setup
|
651
|
$curl = curl_init();
|
652
|
curl_setopt($curl, CURLOPT_USERAGENT, 'Route53/php');
|
653
|
|
654
|
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, ($this->r53->verifyHost() ? 1 : 0));
|
655
|
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, ($this->r53->verifyPeer() ? 1 : 0));
|
656
|
|
657
|
curl_setopt($curl, CURLOPT_URL, $url);
|
658
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
|
659
|
curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
|
660
|
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
661
|
|
662
|
// Request types
|
663
|
switch ($this->verb) {
|
664
|
case 'GET': break;
|
665
|
case 'POST':
|
666
|
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
|
667
|
if(strlen($this->data) > 0) {
|
668
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
|
669
|
$headers[] = 'Content-Type: text/plain';
|
670
|
$headers[] = 'Content-Length: '.strlen($this->data);
|
671
|
}
|
672
|
break;
|
673
|
case 'DELETE':
|
674
|
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
675
|
break;
|
676
|
default: break;
|
677
|
}
|
678
|
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
679
|
curl_setopt($curl, CURLOPT_HEADER, false);
|
680
|
|
681
|
// Execute, grab errors
|
682
|
if (curl_exec($curl)) {
|
683
|
$this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
684
|
} else {
|
685
|
$this->response->error = array(
|
686
|
'curl' => true,
|
687
|
'code' => curl_errno($curl),
|
688
|
'message' => curl_error($curl),
|
689
|
'resource' => $this->resource
|
690
|
);
|
691
|
}
|
692
|
|
693
|
@curl_close($curl);
|
694
|
|
695
|
// Parse body into XML
|
696
|
if ($this->response->error === false && isset($this->response->body)) {
|
697
|
$this->response->body = simplexml_load_string($this->response->body);
|
698
|
|
699
|
// Grab Route53 errors
|
700
|
if (!in_array($this->response->code, array(200, 201, 202, 204))
|
701
|
&& isset($this->response->body->Error)) {
|
702
|
$error = $this->response->body->Error;
|
703
|
$output = array();
|
704
|
$output['curl'] = false;
|
705
|
$output['Error'] = array();
|
706
|
$output['Error']['Type'] = (string)$error->Type;
|
707
|
$output['Error']['Code'] = (string)$error->Code;
|
708
|
$output['Error']['Message'] = (string)$error->Message;
|
709
|
$output['RequestId'] = (string)$this->response->body->RequestId;
|
710
|
|
711
|
$this->response->error = $output;
|
712
|
unset($this->response->body);
|
713
|
}
|
714
|
}
|
715
|
|
716
|
return $this->response;
|
717
|
}
|
718
|
|
719
|
/**
|
720
|
* CURL write callback
|
721
|
*
|
722
|
* @param resource &$curl CURL resource
|
723
|
* @param string &$data Data
|
724
|
* @return integer
|
725
|
*/
|
726
|
private function __responseWriteCallback(&$curl, &$data) {
|
727
|
$this->response->body .= $data;
|
728
|
return strlen($data);
|
729
|
}
|
730
|
|
731
|
/**
|
732
|
* Contributed by afx114
|
733
|
* URL encode the parameters as per http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?Query_QueryAuth.html
|
734
|
* 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
|
735
|
* See: http://www.morganney.com/blog/API/AWS-Product-Advertising-API-Requires-a-Signed-Request.php
|
736
|
*
|
737
|
* @param string $var String to encode
|
738
|
* @return string
|
739
|
*/
|
740
|
private function __customUrlEncode($var) {
|
741
|
return str_replace('%7E', '~', rawurlencode($var));
|
742
|
}
|
743
|
|
744
|
/**
|
745
|
* Generate the auth string using Hmac-SHA256
|
746
|
*
|
747
|
* @internal Used by SimpleDBRequest::getResponse()
|
748
|
* @param string $string String to sign
|
749
|
* @return string
|
750
|
*/
|
751
|
private function __getSignature($string) {
|
752
|
return base64_encode(hash_hmac('sha256', $string, $this->r53->getSecretKey(), true));
|
753
|
}
|
754
|
}
|