File Coverage

blib/lib/Net/RNDC/Session.pm
Criterion Covered Total %
statement 104 115 90.4
branch 31 40 77.5
condition 7 10 70.0
subroutine 18 18 100.0
pod 0 3 0.0
total 160 186 86.0


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