File Coverage

blib/lib/UltraDNS.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package UltraDNS;
2              
3 6     6   169629 use warnings;
  6         15  
  6         182  
4 6     6   32 use strict;
  6         10  
  6         176  
5 6     6   37 use Carp;
  6         16  
  6         793  
6              
7             our $VERSION = '0.06';
8              
9             =head1 NAME
10              
11             UltraDNS - Client API for the NeuStar UltraDNS Transaction Protocol
12              
13             =head1 SYNOPSIS
14              
15             use UltraDNS;
16              
17             # establish a secure connection
18             my $udns = UltraDNS->connect("$host:$port", $sponsor, $username, $password);
19              
20             # Queue up one or more actions to be performed
21             $udns->CreateARecord($zone);
22             $udns->CreateCNAMERecord($zone);
23              
24             # Send actions as a single transaction
25             $udns->commit(...); # throws exception on error
26              
27             # queue up and commit more requests on the same connection
28              
29             Getting multiple results:
30              
31             # Actions can return results. Each return value is a reference
32             # to where the result will be stored when commit() is called.
33             $result_ref1 = $udns->GetZoneInfo($zone);
34             $result_ref2 = $udns->GetMXRecordsOfZone($zone);
35              
36             $udns->commit(...);
37              
38             # $result_ref values above now refer to the RPC::XML results for
39             # each method, use ($$result_ref1)->value to get the value
40              
41             Getting a single result:
42              
43             # utility method that calls commit and returns the dereferenced result
44             $result = $udns->do( ...some method that queues a request... );
45              
46             $result = $udns->do( $udns->AutoSerialUpdateState );
47             # $result is either 1 or 0 (no need to deref or call value() method)
48              
49             # also works for multiple method calls
50             @results = $udns->do( ...multiple method calls... );
51              
52             =head1 DESCRIPTION
53              
54             A simple and efficient client for the NeuStar UltraDNS Transaction Protocol as
55             defined in L (version 3.0, dated
56             September 5, 2008).
57              
58             All requests are batched and performed in transactions. A single secure
59             connection is established and reused for any number of transactions.
60             Multiple concurrent connections can be used if required.
61              
62             All errors are reported via exceptions.
63              
64             =head2 STATUS
65              
66             All UltraDNS methods are supported.
67              
68             Experimentation and feedback are encouraged.
69              
70             =head1 METHODS
71              
72             =cut
73              
74 6     6   5413 use Symbol qw(gensym);
  6         13032  
  6         424  
75 6     6   6707 use Data::Dumper;
  6         65695  
  6         471  
76 6     6   8561 use IO::Socket::INET;
  6         182429  
  6         50  
77 6     6   12192 use Net::SSLeay qw(die_now die_if_ssl_error);
  6         76182  
  6         4051  
78              
79 6     6   4284 use UltraDNS::Parser;
  0            
  0            
80             use UltraDNS::Type;
81             use UltraDNS::Methods;
82              
83              
84             # initialise Net::SSLeay
85             Net::SSLeay::load_error_strings();
86             Net::SSLeay::SSLeay_add_ssl_algorithms();
87             Net::SSLeay::randomize();
88              
89              
90             =head2 connect
91              
92             $udns = UltraDNS->connect($host_and_port, $sponsor, $username, $password, $attr);
93              
94             Establish a secure C connection to the specified UltraDNS host and port,
95             and login using the specified $sponsor, $username, $password.
96              
97             Returns an C object. Throws an exception on error.
98              
99             The optional $attr parameter is a reference to a hash of attributes:
100              
101             =over 4
102              
103             =item trace
104              
105             Specifies the integer trace (debug) level. 0 for none, 1 for basic tracing, and
106             2 and above for more detailed, and more verbose, tracing. Trace messages are
107             output via C.
108              
109             =item ssl_trace
110              
111             Sets $Net::SSLeay::trace. 0=no warns, 1=only errors, 2=ciphers, 3=progress, 4=dump data.
112             See L for more information.
113              
114             =item version
115              
116             Specifies the protocol version argument value used in the C request.
117              
118             =back
119              
120             See L for a list of the UltraDNS Transaction Protocol
121             methods you can call once a connection is established.
122              
123             =cut
124              
125             sub connect { ## no critic (ProhibitBuiltinHomonyms)
126             my ($class, $host_and_port, $sponsor, $username, $password, $attr) = @_;
127             $host_and_port ||= "api.ultradns.net:8755";
128              
129             my $version = $attr->{version} || '3.0';
130              
131             # create an underlying raw socket and connect it to the UltraDNS server
132             my $socket = IO::Socket::INET->new(PeerAddr => $host_and_port)
133             or croak ("Error connecting to $host_and_port: $!");
134              
135             # create a new SSL instance and link it to the socket
136             $Net::SSLeay::trace = $attr->{ssl_trace} if $attr->{ssl_trace};
137             my $ctx = Net::SSLeay::CTX_new() or die_now("Failed to create SSL_CTX $!");
138             my $ssl = Net::SSLeay::new($ctx) or die_now("Failed to create SSL $!");
139             Net::SSLeay::set_fd($ssl, fileno($socket)); # Must use fileno
140              
141             # connect and negotiate at the SSL level
142             my $resp = Net::SSLeay::connect($ssl);
143             die_if_ssl_error("SSL connect failed");
144              
145             my $self = bless {
146             peer_addr => $host_and_port,
147             user => $username,
148             fh => $socket, # just to hold ref to underlying socket
149             ssl => $ssl,
150             queue => [],
151             err => 0,
152             errstr=> '',
153             } => $class;
154              
155             $self->trace($attr->{trace} || $ENV{ULTRADNS_TRACE});
156              
157             $self->_send_xml( join "",
158             "",
159             "",
160             ); # no reply at this point
161              
162             $self->OpenConnection($sponsor, $username, $password, $version);
163             $self->NoAutoCommit(); # for transaction safety
164              
165             my $commit = $self->commit;
166             # sanity check - probably not needed as commit() throws an exception
167             # if the server returns a fault response
168             $self->_throw_error("Initial setup failed: $$commit")
169             unless $$commit eq 'Transaction succeeded';
170              
171             return $self;
172             }
173              
174             sub DESTROY {
175             # nothing extra needed, just let perl look after it
176             }
177              
178              
179             =head2 commit
180              
181             $udns->commit;
182              
183             Submits the queued requests. An exception is thown on error.
184              
185             =cut
186              
187             sub commit {
188             my ($self) = @_;
189              
190             my $queue = $self->{queue};
191             # reset the object state
192             $self->{queue} = [];
193             $self->{err} = 0;
194             $self->{errstr} = '';
195              
196             $self->_trace(sprintf "committing %d requests (+1 transaction)\n", scalar @$queue);
197              
198             my $xml = join "\n", map { $_->{xml} } @$queue;
199             $self->_send_xml( "\n$xml\n" );
200              
201             $self->{stats}{transactions}++;
202              
203             my ($responses, $response_xml) = $self->_get_responses;
204              
205             # Shift the @$responses into the result slots in @$queue.
206             # We expect one more item in @$responses than @$queue because @$responses
207             # should have an extra 'Transaction succeeded' at the end.
208             while (@$queue) {
209             croak "Didn't get responses for all methods in transaction"
210             if @$responses == 0;
211             my $slot = shift @$queue;
212             my $response = shift @$responses;
213             my $xml = shift @$response_xml;
214             $self->_throw_error("Unexpected response: $response ($xml)")
215             if not UNIVERSAL::can($response, 'is_fault');
216             $self->_throw_fault($response->value, $slot->{shortmess})
217             if $response->is_fault;
218             $slot->{result} = $response->value;
219             }
220             my $response = shift @$responses;
221             $self->_throw_fault($response->value, "commit")
222             if $response->is_fault;
223             $self->_throw_error("Unexpected extra responses after commit")
224             if @$responses;
225              
226             return $response->value;
227             }
228              
229              
230             sub _throw_fault {
231             my ($self, $fault, $what) = @_;
232             # record the error details in the object
233             my $err = $self->{err} = $fault->code;
234             my $errstr = $self->{errstr} = $fault->string;
235             my $msg = "$what failed with server-side error $err: $errstr";
236             return $self->_throw_error($msg); # doesn't return
237             }
238              
239              
240             =head2 rollback
241              
242             $udns->rollback;
243              
244             Discards the queued requests.
245              
246             =cut
247              
248             sub rollback {
249             shift->{queue} = [];
250             return;
251             }
252              
253              
254             =head2 do
255              
256             $result = $udns->do( $udns->SomeMethodThatReturnsAResult(...) );
257              
258             A convienience method that calls commit() and returns the de-referenced
259             argument. The one-line call has the same effect as these three lines:
260              
261             $result_ref = $udns->SomeMethodThatReturnsAResult(...);
262             $udns->commit;
263             $result = $$result_ref; # de-reference to get return value
264              
265             but is much more convienient when you just want to get a value from the server.
266              
267             Multiple calls can be combined into a single request like this:
268              
269             my ($a, $b, $c) = $udns->do(
270             $udns->MethodReturningA(...),
271             $udns->MethodReturningB(...),
272             $udns->MethodReturningC(...)
273             );
274              
275             =cut
276              
277             sub do {
278             my ($self, @result_refs) = @_;
279             croak "Can't call do() without an UltraDNS object reference"
280             unless ref $self and UNIVERSAL::isa($self, __PACKAGE__);
281              
282             my $queue = $self->{queue};
283              
284             $self->_throw_error(sprintf "do() called with %d arguments but %d actions are queued (%s)",
285             scalar @result_refs, scalar @$queue,
286             join(", ", map { "$_->{method} at $_->{shortmess}" } @$queue) )
287             if scalar @result_refs != scalar @$queue;
288              
289             $self->_throw_error("do() called in scalar context but with more than one argument")
290             if not wantarray and @result_refs > 1;
291              
292             # we're asked to do nothing, so we return nothing
293             return unless @result_refs;
294              
295             $self->commit;
296              
297             my @results = map { ($$_)->value } @result_refs;
298              
299             return $results[0] if not wantarray;
300             return @results;
301             }
302              
303              
304             =head2 eval
305              
306             Just like the L method except any exception will be caught.
307             This is useful for cases where an error is expected, such as deleting a record
308             in the server that may not exist.
309              
310             XXX currently it catches all exceptions, it's expected that in future it will
311             only catch exceptions due to server-reported error.
312              
313             =cut
314              
315             sub eval { ## no critic (ProhibitBuiltinHomonyms)
316             my @results = eval { shift->do( @_ ) };
317             return @results; # empty if do() threw an exception
318             }
319              
320              
321             =head2 err
322              
323             $err = $udns->err;
324              
325             Returns the error code from the server for the last transaction, else 0.
326              
327             =head2 errstr
328              
329             $errstr = $udns->errstr;
330              
331             Returns the error message from the server for the last transaction, else an empty string.
332              
333             =cut
334              
335             sub err { return shift->{err} }
336             sub errstr { return shift->{errstr} }
337              
338              
339             =head2 trace
340              
341             $udns->trace($level);
342             $prev = $udns->trace($level);
343             $prev = $udns->trace;
344              
345             Sets the new trace level, if a value is supplied.
346             0 = off, 1 = basic overview, 2+ = more details.
347             Returns the previous level.
348              
349             =cut
350              
351             sub trace { ## no critic (RequireArgUnpacking)
352             my $self = shift;
353             my $prev = $self->{trace} || 0;
354             $self->{trace} = shift || 0 if @_;
355             $self->_trace("trace level set to $self->{trace}") if $self->{trace} or $prev;
356             return $prev;
357             }
358              
359              
360             # ---
361              
362              
363             sub _description {
364             my $self = shift;
365             return sprintf "UltraDNS %s@%s", $self->{user}, $self->{peer_addr};
366             }
367              
368              
369             # _throw_error message shouldn't have newline at end
370             sub _throw_error {
371             my $self = shift;
372             $self->_trace("error: @_\n");
373             croak $self->_description . " error: @_";
374             }
375             sub _warn { carp shift->_description . ": @_\n"; return }
376             # _trace message argument should have newline at end
377             sub _trace { warn "UltraDNS: @_" if shift->{trace}; return }
378              
379              
380             sub _send_xml {
381             my ($self, $xml) = @_;
382             $self->_trace("_send_xml $xml") if $self->{trace} >= 2;
383              
384             Net::SSLeay::write($self->{ssl}, "$xml\r\n")
385             or $self->_throw_error("sending request: $!");
386              
387             return;
388             }
389              
390              
391             sub _get_xml {
392             my $self = shift;
393              
394             $self->_trace("_get_xml awaiting response") if $self->{trace} >= 2;
395              
396             # if we always use a transaction then we can use (note the plural)
397             # to identify the end of the server response to our request, saving ourselves
398             # a whole bunch of headaches and inefficiencies
399             my $response_body = Net::SSLeay::ssl_read_until($self->{ssl}, "");
400             $self->_trace(Dumper($response_body)) if $self->{trace} >= 2;
401              
402             return $response_body;
403             }
404              
405              
406             sub _get_responses {
407             my ($self) = @_;
408              
409             my $response_body = $self->_get_xml;
410              
411             # RPC::XML can't handle the UltraDNS methodResponses (plural)
412             # so we chop out and process each individual methodResponse in turn
413             my @response_xml;
414             my @responses;
415             while ($response_body =~ s{(.*?)}{}m) {
416             my $xml = $1;
417             push @response_xml, $xml;
418             my $resp = UltraDNS::Parser->_parse_rpc_xml($xml);
419             push @responses, $resp;
420             print "XML: $xml:\nPerl: ".Dumper($resp) if $self->{trace} >= 3;
421             }
422              
423             $self->_throw_error("No responses found in $response_body")
424             unless @responses;
425             $self->_trace("_get_responses received ".scalar(@responses)." responses")
426             if $self->{trace} >= 2;
427              
428             # cleanse and sanity check the remaining rump of $response_body
429             $response_body =~ s{<\?xml version=".*?"\?>}{};
430             $response_body =~ s{\s*\s*\s*}{};
431             if ($response_body ne '') {
432             $self->_warn("Unprocessed remnants in response body: '$response_body'");
433             }
434            
435             return \@responses unless wantarray;
436             return (\@responses, \@response_xml);
437             }
438              
439              
440             sub AUTOLOAD { ## no critic (RequireArgUnpacking)
441             (my $method = our $AUTOLOAD) =~ s/.*::(?:UDNS_)?//;
442             my $self = shift;
443              
444             # sanity check to avoid obscure errors when users do odd things
445             croak sprintf "Can't call %s->%s() because '%s' isn't an UltraDNS object reference",
446             $self, $method, $self
447             unless ref $self and UNIVERSAL::isa($self, __PACKAGE__);
448              
449             return $self->_enqueue_method_call($method, \@_);
450             }
451              
452              
453             sub _shortmess { # much faster version of Carp::shortmess
454             my ($self, $what) = @_;
455              
456             my ($pkg, $file, $line, $level);
457             do { ($pkg, $file, $line) = caller(++$level) } while $pkg =~ /^UltraDNS\b/;
458              
459             my $shortmess = "$file line $line";
460             $shortmess = "$what at $shortmess" if $what;
461              
462             return $shortmess;
463             }
464              
465              
466             sub _enqueue_method_call {
467             my ($self, $method, $args) = @_;
468              
469             my $shortmess = $self->_shortmess($method);
470             $self->_trace($shortmess)
471             if $self->{trace};
472             $self->{stats}{methods}{$method}++;
473              
474             my $xml = $self->_xml_for_method_call($method, $args);
475              
476             return $self->_enqueue_xml($xml, {
477             method => $method,
478             args => $args,
479             shortmess => $shortmess,
480             });
481             }
482              
483              
484             sub _enqueue_xml {
485             my ($self, $xml, $meta) = @_;
486             my $queue = $self->{queue};
487             push @$queue, { %{ $meta || {} }, xml => $xml, result => undef };
488             # return reference to the slot that will hold the result value for this method call
489             return \$queue->[-1]{result};
490             }
491              
492              
493             sub _xml_for_method_call {
494             my ($self, $method, $args) = @_;
495              
496             my $method_info = UltraDNS::Methods->_method_spec("UDNS_$method")
497             or croak "Can't call unknown method '$method'"; # XXX could allow later
498              
499             my $arg_info = $method_info->{arg_info};
500             if (@$args < @$arg_info) {
501             croak sprintf "%s called with too few parameters (has %d parameters but %d only arguments were given)",
502             $method, scalar @$arg_info, scalar @$args;
503             }
504             if (@$args > @$arg_info) {
505             croak sprintf "%s called with too many parameters (has %d parameters but %d arguments were given)",
506             $method, scalar @$arg_info, scalar @$args
507             unless $method_info->{last_arg_repeats};
508             # clone and pad out arg_info with copies of the final arg
509             $arg_info = [ @$arg_info ];
510             push @$arg_info, $arg_info->[-1] while @$arg_info < @$args;
511             }
512              
513             my @value_xml;
514             for my $arg_info (@$arg_info) {
515             my $value = shift @$args;
516             my $class = "RPC::XML::$arg_info->{type}";
517             my $value_obj = $class->new($value);
518             push @value_xml, sprintf "%s\n",
519             $value_obj->as_string;
520             }
521              
522             return "UDNS_$method\n@value_xml\n";
523             }
524              
525              
526             1;
527              
528             __END__