File Coverage

blib/lib/Protocol/DBus/Authn.pm
Criterion Covered Total %
statement 98 141 69.5
branch 18 50 36.0
condition 5 18 27.7
subroutine 16 21 76.1
pod 0 5 0.0
total 137 235 58.3


line stmt bran cond sub pod time code
1             package Protocol::DBus::Authn;
2              
3 9     9   1903 use strict;
  9         26  
  9         260  
4 9     9   76 use warnings;
  9         16  
  9         269  
5              
6 9     9   3930 use IO::Framed ();
  9         28704  
  9         179  
7 9     9   62 use Module::Load ();
  9         18  
  9         116  
8 9     9   37 use Socket ();
  9         18  
  9         116  
9              
10 9     9   3547 use Protocol::DBus::X ();
  9         27  
  9         267  
11              
12 9     9   54 use constant _CRLF => "\x0d\x0a";
  9         22  
  9         550  
13              
14 9     9   54 use constant DEBUG => 0;
  9         18  
  9         14540  
15              
16             sub new {
17 2     2 0 372685 my ($class, %opts) = @_;
18              
19 2         66 my @missing = grep { !$opts{$_} } qw( socket mechanism );
  4         80  
20 2 50       81 die "Need: @missing" if @missing;
21              
22 2         83 $opts{"_$_"} = delete $opts{$_} for keys %opts;
23              
24 2         244 $opts{'_can_pass_unix_fd'} = Socket::MsgHdr->can('new');
25 2   33     67 $opts{'_can_pass_unix_fd'} &&= Socket->can('SCM_RIGHTS');
26 2   33     44 $opts{'_can_pass_unix_fd'} &&= _is_unix_socket($opts{'_socket'});
27              
28 2         102 $opts{'_io'} = IO::Framed->new( $opts{'_socket'} )->enable_write_queue();
29              
30 2         458 my $self = bless \%opts, $class;
31              
32 2 50       38 $self->_set_mechanism( $opts{'_mechanism'} ) or do {
33 0         0 die "“$opts{'_mechanism'}” is not a valid authn mechanism.";
34             };
35              
36 2         17 return $self;
37             }
38              
39             sub _set_mechanism {
40 2     2   20 my ($self, $mechanism) = @_;
41              
42 2 50       31 if (!ref $mechanism) {
43 2         30 my $module = __PACKAGE__ . "::Mechanism::$mechanism";
44              
45 2         7 my $err = $@;
46 2 50       13 if (!eval { Module::Load::load($module); 1 } ) {
  2         49  
  2         534  
47 0         0 DEBUG && print STDERR "Failed to load $mechanism authn module: $@";
48 0         0 return 0;
49             }
50 2         23 $@ = $err;
51              
52 2         47 $self->{'_mechanism'} = $module->new();
53             }
54              
55 2         38 $self->{'_xaction'} = $self->_create_xaction();
56              
57 2         17 return 1;
58             }
59              
60             sub negotiated_unix_fd {
61 0 0   0 0 0 return $_[0]->{'_negotiated_unix_fd'} ? 1 : 0;
62             }
63              
64             # Whether a send is pending (1) or a receive (0).
65             sub pending_send {
66 0     0 0 0 my ($self) = @_;
67              
68 0         0 my $next_is_receive = $self->{'_xaction'}[0];
69 0   0     0 $next_is_receive &&= $next_is_receive->[0];
70              
71 0 0       0 if (!defined $next_is_receive) {
72 0         0 die "Authn transaction is done!";
73             }
74              
75 0         0 return !$next_is_receive;
76             }
77              
78             sub go {
79 2     2 0 8 my ($self) = @_;
80              
81 2         5 my $s = $self->{'_socket'};
82              
83             # Don’t send_initial() if !must_send_initial().
84 2   33     48 $self->{'_sent_initial'} ||= !$self->{'_mechanism'}->must_send_initial() || $self->{'_mechanism'}->send_initial($s);
      33        
85              
86 2 50       8 if ($self->{'_sent_initial'}) {
87             LINES:
88             {
89 2 50       4 if ( $self->{'_io'}->get_write_queue_count() ) {
  2         40  
90 0 0       0 $self->flush_write_queue() or last LINES;
91             }
92              
93 2         30 my $dollar_at = $@;
94 2         12 my $ok = eval {
95 2         10 while ( my $cur = $self->{'_xaction'}[0] ) {
96 8 100       34 if ($cur->[0]) {
97 3 50       11 my $line = $self->_read_line() or last LINES;
98 3         16 $cur->[1]->($self, $line);
99             }
100             else {
101 5         10 my @line_parts;
102              
103 5 100       20 if ('CODE' eq ref $cur->[1]) {
104 1         4 @line_parts = $cur->[1]->($self);
105             }
106             else {
107 4         16 @line_parts = @{$cur}[ 1 .. $#$cur ];
  4         16  
108             }
109              
110 5 50       36 $self->_send_line("@line_parts") or last LINES;
111              
112 5         3459 push @{ $self->{'_tried_mechanism'} }, $self->{'_mechanism'}->label();
  5         46  
113             }
114              
115 8         15 shift @{ $self->{'_xaction'} };
  8         39  
116             }
117              
118 2         7 1;
119             };
120              
121 2 50       8 if (!$ok) {
122 0         0 my $err = $@;
123 0 0       0 if (eval { $err->isa('Protocol::DBus::X::Rejected') }) {
  0         0  
124              
125 0         0 $self->{'_mechanism'}->on_rejected();
126              
127 0         0 my @to_try;
128              
129 0         0 for my $mech ( @{ $err->get('mechanisms') } ) {
  0         0  
130 0 0       0 if (!grep { $_ eq $mech } @{ $self->{'_tried_mechanism'} }) {
  0         0  
  0         0  
131 0         0 push @to_try, $mech;
132             }
133             }
134              
135 0         0 while (my $mech = shift @to_try) {
136 0 0       0 if ($self->_set_mechanism($mech)) {
137 0         0 redo LINES;
138             }
139             }
140              
141 0         0 die "Exhausted all authentication mechanisms! (@{ $self->{'_tried_mechanism'} })";
  0         0  
142             }
143             else {
144 0         0 local $@ = $err;
145 0         0 die;
146             }
147             }
148              
149 2         56 return 1;
150             }
151             }
152              
153 0         0 return undef;
154             }
155              
156             sub cancel {
157 0     0 0 0 my ($self) = @_;
158              
159 0         0 die 'unimplemented';
160             }
161              
162             sub _create_xaction {
163 2     2   8 my ($self) = @_;
164              
165 2         18 my $auth_label = 'AUTH';
166              
167             # Unless the mechanism sends its own initial NUL, might as well use the
168             # same system call to send the initial NUL as we use to send the AUTH.
169 2 50 33     79 if (!$self->{'_sent_initial'} && !$self->{'_mechanism'}->must_send_initial()) {
170 2         9 substr( $auth_label, 0, 0 ) = "\0";
171             }
172              
173             # 0 = send; 1 = receive
174             my @xaction = (
175             [ 0 => $auth_label, $self->{'_mechanism'}->label(), $self->{'_mechanism'}->INITIAL_RESPONSE() ],
176              
177             # e.g., for exchange of DATA
178 2         61 $self->{'_mechanism'}->AFTER_AUTH(),
179              
180             [ 1 => \&_consume_ok ],
181             );
182              
183 2 50       20 if ($self->{'_can_pass_unix_fd'}) {
184 0         0 push @xaction, (
185             [ 0 => 'NEGOTIATE_UNIX_FD' ],
186             [ 1 => \&_consume_agree_unix_fd ],
187             );
188             }
189              
190 2         21 push @xaction, [ 0 => 'BEGIN' ];
191              
192 2         31 return \@xaction;
193             }
194              
195             sub _consume_agree_unix_fd {
196 0     0   0 my ($self, $line) = @_;
197              
198 0 0       0 if ($line eq 'AGREE_UNIX_FD') {
    0          
199 0         0 $self->{'_negotiated_unix_fd'} = 1;
200             }
201             elsif (index($line, 'ERROR ') == 0) {
202 0         0 warn "Server rejected unix fd passing: " . substr($line, 6) . $/;
203             }
204              
205 0         0 return;
206             }
207              
208             sub _consume_ok {
209 2     2   9 my ($self, $line) = @_;
210              
211 2 50       10 if (index($line, 'OK ') == 0) {
212 2         21 $self->{'_server_guid'} = substr($line, 3);
213             }
214             else {
215 0         0 die "Unrecognized response: $line";
216             }
217              
218 2         8 return;
219             }
220              
221             sub _send_line {
222 5     5   23 my ($self) = @_;
223              
224 5         8 DEBUG() && print STDERR "AUTHN SENDING: [$_[1]]$/";
225              
226 5         63 $self->{'_io'}->write( $_[1] . _CRLF() );
227              
228 5         99 return $self->_flush_write_queue();
229             }
230              
231             sub _flush_write_queue {
232 5     5   12 my ($self) = @_;
233              
234 5         150 local $SIG{'PIPE'} = 'IGNORE';
235              
236 5         48 return $self->{'_io'}->flush_write_queue();
237             }
238              
239             sub _read_line {
240 3     3   50 my $line;
241              
242 3         23 DEBUG() && print STDERR "AUTHN RECEIVING …$/";
243              
244 3 50       62 if ($line = $_[0]->{'_io'}->read_until("\x0d\x0a")) {
245 3         16077 substr( $line, -2 ) = q<>;
246              
247 3         14 DEBUG() && print STDERR "AUTHN RECEIVED: [$line]$/";
248              
249 3 50       17 if (0 == index( $line, 'REJECTED ')) {
250 0         0 die Protocol::DBus::X->create(
251             'Rejected',
252             split( m< >, substr( $line, 9 ) ),
253             );
254             }
255             }
256              
257 3         14 return $line;
258             }
259              
260             sub _is_unix_socket {
261 0     0     my ($sk) = @_;
262              
263 0 0         my $sname = getsockname($sk) or die "getsockname(): $!";
264              
265 0           return Socket::sockaddr_family($sname) == Socket::AF_UNIX();
266             }
267              
268             1;