File Coverage

blib/lib/Net/RNDC/Session.pm
Criterion Covered Total %
statement 104 115 90.4
branch 31 40 77.5
condition 9 10 90.0
subroutine 18 18 100.0
pod 0 3 0.0
total 162 186 87.1


line stmt bran cond sub pod time code
1             package Net::RNDC::Session;
2             {
3             $Net::RNDC::Session::VERSION = '0.003';
4             }
5              
6 2     2   65255 use strict;
  2         4  
  2         83  
7 2     2   12 use warnings;
  2         5  
  2         59  
8              
9 2     2   1293 use Net::RNDC::Packet;
  2         7  
  2         45  
10              
11 2     2   117 use Carp qw(croak);
  2         5  
  2         4004  
12              
13             # Controls the flow in next(). undef means next() should never
14             # be called if we've reached this state
15             my %states = (
16             start => '_got_start',
17             want_read => '_got_read',
18             want_write => '_got_write',
19             want_error => undef,
20             want_finish => undef,
21             );
22              
23             sub new {
24 14     14 0 12243 my ($class, %args) = @_;
25              
26 14         48 my @required_subs = qw(
27             want_read
28             want_write
29             want_finish
30             want_error
31             );
32              
33 14         18 my @optional_subs = qw(
34             );
35              
36 14         23 my @required_args = qw(
37             key
38             command
39             );
40              
41 14         23 my @optional_args = qw(
42             is_client
43             is_server
44             );
45              
46 14         25 for my $r (@required_subs, @required_args) {
47 69 100       153 unless (exists $args{$r}) {
48 6         86 croak("Missing required argument '$r'");
49             }
50             }
51              
52 8 100 100     38 unless (exists $args{is_client} || exists $args{is_server}) {
53 1         14 croak("Argument 'is_client' or 'is_server' must be defined");
54             }
55              
56 7         12 for my $r (@required_subs, @optional_subs) {
57 22 50       47 next unless exists $args{$r};
58              
59 22 100 100     71 unless ((ref $args{$r} || '') eq 'CODE') {
60 4         43 croak("Argument '$r' is not a code ref");
61             }
62             }
63              
64 3 100 100     17 if (exists $args{is_client} && exists $args{is_server}) {
65 1         9 croak("Argument 'is_client' cannot be mixed with 'is_server'");
66             }
67              
68 14         41 my %obj = map {
69 16         31 $_ => $args{$_}
70 2         5 } grep { exists $args{$_} } (@required_subs, @optional_subs, @required_args, @optional_args);
71              
72 2 100       9 if (exists $args{is_client}) {
73 1         2 $obj{is_client} = 1;
74             } else {
75 1         3 $obj{is_server} = 1;
76             }
77              
78 2         6 my $obj = bless \%obj, $class;
79              
80             # Base state
81 2         7 $obj->_init;
82              
83 2         10 return $obj;
84             }
85              
86             # Maybe open up to public as reset()?
87             sub _init {
88 2     2   3 my ($self) = @_;
89              
90             # Have we sent our syn/ack opener?
91 2         12 $self->{nonce} = 0;
92              
93 2         6 $self->_state('start');
94             }
95              
96             # Set/retrieve state
97             sub _state {
98 25     25   41 my ($self, $state) = @_;
99              
100 25 100       53 if ($state) {
101 13 50       32 unless (exists $states{$state}) {
102 0         0 croak("Unknown state $state requested");
103             }
104              
105 13         27 $self->{state} = $state;
106             }
107              
108 25         57 return $self->{state};
109             }
110              
111 10     10   46 sub _is_client { return $_[0]->{'is_client'} }
112 2     2   7 sub _is_server { return $_[0]->{'is_server'} }
113 8     8   59 sub _key { return $_[0]->{'key'} }
114 4     4   23 sub _nonce { return $_[0]->{'nonce'} }
115 2     2   15 sub _command { return $_[0]->{'command'} }
116              
117             # Entry point. Always.
118             sub start {
119 2     2 0 10 my ($self) = @_;
120              
121 2 50       6 unless (my $state = $self->_state eq 'start') {
122 0         0 croak("Attempt to re-use an existing session in state '$state'");
123             }
124              
125 2         7 $self->next;
126             }
127              
128             # Move things along. Pass in data if needed
129             sub next {
130 10     10 0 48 my ($self, $data) = @_;
131              
132 10         30 my $sub = $states{$self->_state};
133              
134 10 50       23 unless ($sub) {
135 0         0 croak("next() called on bad state '" . $self->_state . "'");
136             }
137              
138 10         56 $self->$sub($data);
139              
140 10         95 return;
141             }
142              
143             # _got subs are called after a want_* sub has been called and next() has been used
144              
145             # Starting out
146             sub _got_start {
147 2     2   3 my ($self, $data) = @_;
148              
149 2 100       6 if ($self->_is_client) {
150             # Client step 1: send a request packet with no data section
151 1         4 my $packet = Net::RNDC::Packet->new(
152             key => $self->_key,
153             );
154              
155 1         4 $self->_state('want_write');
156              
157 1         6 return $self->_run_want('want_write', $packet->data, $packet);
158             } else {
159             # Server step 1: expect a packet with no data section
160 1         9 $self->_state('want_read');
161              
162 1         4 return $self->_run_want('want_read');
163             }
164             }
165              
166             sub _got_read {
167 4     4   7 my ($self, $data) = @_;
168              
169 4 100       9 if ($self->_is_client) {
170 2         18 my $packet = Net::RNDC::Packet->new(key => $self->_key);
171              
172 2 50       9 if (!$packet->parse($data)) {
173 0         0 $self->_state('want_error');
174              
175 0         0 return $self->_run_want('want_error', $packet->error);
176             }
177              
178 2 100       8 if (! $self->_nonce) {
179             # Client step 2: Parse response, get nonce
180 1         3 $self->{nonce} = 1;
181              
182 1         3 my $nonce = $packet->{data}->{_ctrl}{_nonce};
183              
184             # Client step 3: Send request with nonce/data section
185 1         4 my $packet2 = Net::RNDC::Packet->new(
186             key => $self->_key,
187             nonce => $nonce,
188             data => {type => $self->_command},
189             );
190              
191 1         5 $self->_state('want_write');
192              
193 1         5 return $self->_run_want('want_write', $packet2->data, $packet2);
194             } else {
195             # Client step 4: Read response to command
196 1   50     5 my $response = $packet->{data}{_data}{text} || 'command success';
197              
198 1         4 $self->_state('want_finish');
199              
200 1         5 return $self->_run_want('want_finish', $response);
201             }
202             } else {
203 2         10 my $packet = Net::RNDC::Packet->new(key => $self->_key);
204              
205 2 50       10 if (!$packet->parse($data)) {
206 0         0 $self->_state('want_error');
207              
208 0         0 return $self->_run_want('want_error', $packet->error);
209             }
210              
211 2 100       9 if (! $self->_nonce) {
212 1         2 $self->{nonce} = 1;
213              
214 1         5 my $nonce = int(rand(2**32));
215              
216 1         4 $self->{_nonce_data} = $nonce;
217              
218 1         2 my $challenge = Net::RNDC::Packet->new(
219             key => $self->_key,
220             nonce => $nonce,
221             );
222              
223 1         4 $self->_state('want_write');
224              
225 1         4 return $self->_run_want('want_write', $challenge->data, $challenge);
226             } else {
227 1         3 my $nonce = $self->{_nonce_data};
228              
229             # TODO: Add time/expiry checking
230             # Invalid: (_tim + clockskew < now || _tim - clockskew > now)
231             # Invalid: now > exp
232             # Also check serial?
233              
234 1 50       12 unless ($packet->{data}->{_ctrl}{_nonce}) {
235 0         0 $self->_state('want_error');
236              
237 0         0 return $self->_run_want('want_error', "Client nonce not set");
238             }
239              
240 1 50       6 unless ($packet->{data}->{_ctrl}{_nonce} == $nonce) {
241 0         0 $self->_state('want_error');
242              
243 0         0 return $self->_run_want('want_error', "Client nonce does not match");
244             }
245              
246 1         4 my $response = Net::RNDC::Packet->new(
247             key => $self->_key,
248             data => {text => $self->_command},
249             );
250              
251 1         5 $self->_state('want_write');
252              
253 1         5 $self->_run_want('want_write', $response->data, $response);
254              
255 1         5 $self->_state('want_finish');
256              
257 1         5 $self->_run_want('want_finish');
258             }
259             }
260             }
261              
262             sub _got_write {
263 4     4   7 my ($self) = @_;
264              
265             # As a client, after every write we expect a read
266 4 100       22 if ($self->_is_client) {
    50          
267 2         16 $self->_state('want_read');
268              
269 2         11 return $self->_run_want('want_read');
270             } elsif ($self->_is_server) {
271 2         6 $self->_state('want_read');
272              
273 2         8 return $self->_run_want('want_read');
274             }
275             }
276              
277             # Run the requested want_* sub
278             sub _run_want {
279 11     11   24 my ($self, $sub, @args) = @_;
280              
281 11         25 my $ref = $self->{$sub};
282              
283 11         43 $ref->($self, @args);
284             }
285              
286             1;
287             __END__