File Coverage

blib/lib/Thrift/SASL/Transport.pm
Criterion Covered Total %
statement 18 116 15.5
branch 0 60 0.0
condition 0 28 0.0
subroutine 6 20 30.0
pod 0 9 0.0
total 24 233 10.3


line stmt bran cond sub pod time code
1             package Thrift::SASL::Transport;
2             $Thrift::SASL::Transport::VERSION = '0.006';
3 1     1   13931 use strict;
  1         1  
  1         26  
4 1     1   4 use warnings;
  1         1  
  1         21  
5 1     1   484 use Data::Dumper;
  1         6862  
  1         63  
6              
7             # Nasty hack to make the Thrift libs handle the extra 4-bytes
8             # header put by GSSAPI in front of unencoded (auth only) replies
9              
10 1     1   377 use Thrift::BinaryProtocol;
  1         15951  
  1         50  
11             my $real_readMessageBegin = \&Thrift::BinaryProtocol::readMessageBegin;
12             {
13 1     1   6 no warnings 'redefine';
  1         1  
  1         96  
14             *Thrift::BinaryProtocol::readMessageBegin = \&BinaryProtocolOverride_readMessageBegin;
15             }
16              
17             sub BinaryProtocolOverride_readMessageBegin {
18 0     0 0   my $self = shift;
19 0 0 0       if ( $self->{trans}{_sasl_client} && !$self->{trans}{_sasl_encode} ) {
20 0           $self->readI32( \my $foo ); # discard GSSAPI auth header (message length)
21             }
22 0           return $real_readMessageBegin->( $self, @_ );
23             }
24              
25             # end of nasty hack, phew.
26              
27             use constant {
28 1         1030 SASL_START => 1,
29             SASL_OK => 2,
30             SASL_BAD => 3,
31             SASL_ERROR => 4,
32             SASL_COMPLETE => 5,
33 1     1   4 };
  1         1  
34              
35             sub new {
36 0     0 0   my ( $class, $transport, $sasl, $debug ) = @_;
37 0   0       return bless {
38             _transport => $transport,
39             _sasl => $sasl,
40             _debug => $debug || 0,
41             }, $class;
42             }
43              
44             sub _sasl_write {
45 0     0     my ( $self, $code, $payload ) = @_;
46 0   0       $payload //= '';
47 0           print STDERR "<< code $code + payload (@{[bytes::length($payload)]} bytes)\n"
48 0 0         if $self->{_debug};
49 0           $self->{_transport}->write( pack "CN", $code, bytes::length($payload) );
50 0           $self->{_transport}->write($payload);
51 0           $self->{_transport}->flush;
52             }
53              
54             sub _sasl_read {
55 0     0     my ($self) = @_;
56 0           my $data = $self->read(5);
57 0 0         die "No data from server" unless defined $data;
58 0           my ( $code, $length ) = unpack "CN", $data;
59 0 0         if ($length) {
60 0           $data = $self->read($length);
61             }
62             else {
63 0           $data = undef;
64             }
65             print STDERR ">> code $code + response ($length bytes)\n"
66 0 0         if $self->{_debug};
67 0           return ( $code, $data );
68             }
69              
70             sub open {
71 0     0 0   my ($self) = @_;
72 0 0 0       $self->{_transport}->open if !( $self->{_transport} && $self->isOpen );
73 0 0         die "Could not open transport" if !$self->isOpen;
74 0   0       return $self->{_sasl_client} || $self->_sasl_handshake;
75             }
76              
77             sub close {
78 0     0 0   my ($self) = @_;
79 0 0 0       $self->{_transport}->close if $self->{_transport} && $self->isOpen;
80 0           return 1;
81             }
82              
83             sub _sasl_handshake {
84 0     0     my ($self) = @_;
85              
86             print STDERR "SASL start: "
87 0 0         if $self->{_debug};
88 0           $self->_sasl_write( SASL_START, $self->{_sasl}->mechanism );
89              
90             # The socket passed to BufferedTransport was put in that object's
91             # "transport" property, this is a bit confusing imho
92 0           my $client = $self->{_sasl}->client_new( 'hive', $self->{_transport}{transport}{host} );
93 0           my $resp = $client->client_start();
94              
95 0           my $step;
96 0           while ( ++$step ) {
97             print STDERR "SASL step $step: "
98 0 0         if $self->{_debug};
99              
100             #print STDERR Dumper{map {$_ => [$client->$_()]} qw(error code mechanism need_step)};
101 0           $self->_sasl_write( SASL_OK, $resp );
102 0           my ( $code, $data ) = $self->_sasl_read();
103              
104 0 0         if ( $code == SASL_COMPLETE ) {
105             print STDERR "Authentication OK\n"
106 0 0         if $self->{_debug};
107              
108             #$client->client_step($data // '') if $client->need_step;
109 0           last;
110             }
111              
112 0           my $extra_msg = $self->__probe_env_and_xs_sasl_bug( $client );
113              
114 0 0 0       if ( $code == SASL_BAD || $code == SASL_ERROR ) {
115 0 0         die sprintf "Authentication failed: %s > %s%s",
116             $code,
117             $data,
118             $extra_msg ? '. ' . $extra_msg : '',
119             ;
120             }
121              
122 0           $resp = $client->client_step($data);
123 0 0         if ( ! defined $data ) {
124 0 0         die sprintf 'Client rejected authentication%s',
125             $extra_msg ? '. ' . $extra_msg : '',
126             ;
127             }
128             }
129              
130 0           $self->{_sasl_encode_check} = 1;
131 0           $self->{_sasl_client} = $client;
132              
133 0           return $self->{_sasl_client};
134             }
135              
136             sub __probe_env_and_xs_sasl_bug {
137             # See: https://github.com/Perl-Hadoop/Thrift-SASL/issues/1
138             #
139 0     0     my($self, $client) = @_;
140 0 0 0       return if ! $client->isa('Authen::SASL::XS')
141             && ! $client->isa('Authen::SASL::Cyrus')
142             ;
143              
144 0 0 0       return if exists $ENV{USER} || exists $ENV{USERNAME};
145              
146 0           my $sasl_class = ref $client;
147              
148 0           return join ' ',
149             "cyrus-sasl and in turn $sasl_class needs either USER or USERNAME",
150             'environment variable to be present and they seem to be missing',
151             'in your environment.',
152             'The error you have received might be caused by that.',
153             ;
154             }
155              
156             sub write {
157 0     0 0   my $self = shift;
158 0           my $buffer = shift;
159             print STDERR "<< writing " . bytes::length($buffer) . " bytes\n"
160 0 0         if $self->{_debug} > 1;
161 0           $self->{_out_buffer} .= $buffer;
162 0           return 1;
163             }
164              
165             sub read {
166 0     0 0   my $self = shift;
167 0           my @passthru_args = @_;
168             print STDERR ">> reading\n"
169 0 0         if $self->{_debug};
170 0           my $buf = $self->{_transport}->read(@passthru_args);
171 0 0         return $buf if bytes::length($buf) > 0;
172              
173             # not sure about this, it is copied over from the python version
174 0           $self->_read_frame();
175 0           return $self->{_transport}->read(@passthru_args);
176             }
177              
178             # completely unsure about this, taken from the python version when trying to
179             # make the whole thing work, turned out my problem was with the BinaryProtocol
180             # needing a 4-byte offset on readMessageBegin as kerberos auth adds a 4 bytes
181             # header to replies, which was mistakenly used as the expected thrift header.
182             # leaving this in place in case it is actually needed (probably not working
183             # in the current state though)
184             sub _read_frame {
185 0     0     my $self = shift;
186 0           my $header = $self->{_transport}->readAll(4);
187 0           my $length = unpack( "N", $header );
188 0           my $decoded;
189 0 0         if ( $self->{_sasl_encode} ) {
190 0           my $encoded = $header . $self->{_transport}->readAll($length);
191 0 0         my $decoded = $self->{_sasl_client}->decode($encoded)
192             or die 'SASL decode returned nothing';
193              
194             # TODO throw a real per TTransportException like the python version
195             #die "SASL decode error: " .
196             # raise TTransportException(type=TTransportException.UNKNOWN,
197             # message=self.sasl.getError())
198             }
199             else {
200 0           $decoded = $self->{_transport}->readAll($length);
201             }
202 0           $self->{_transport}{rBuf} = $decoded;
203             }
204              
205             sub flush {
206 0     0 0   my $self = shift;
207              
208             print STDERR "<<< flush " . bytes::length( $self->{_out_buffer} ) . " bytes \n"
209 0 0         if $self->{_debug};
210              
211 0 0         if ( $self->{_sasl_encode_check} ) {
212 0           my $encoded = $self->{_sasl_client}->encode( $self->{_out_buffer} );
213 0 0 0       if ( bytes::length($encoded)
214             && bytes::length($encoded) != bytes::length( $self->{_out_buffer} ) )
215             {
216 0           $self->{_sasl_encode} = 1;
217 0 0         print STDERR "GSSAPI Will encode from now on\n" if $self->{_debug};
218             }
219             else {
220 0           $self->{_sasl_encode} = 0;
221 0 0         print STDERR "GSSAPI Will *not* encode from now on\n" if $self->{_debug};
222             }
223 0           $self->{_sasl_encode_check} = undef;
224             }
225 0 0         if ( $self->{_sasl_encode} ) {
226 0           $self->{_out_buffer} = $self->{_sasl_client}->encode( $self->{_out_buffer} );
227             }
228             else {
229             $self->{_out_buffer}
230 0           = pack( "N", bytes::length( $self->{_out_buffer} ) ) . $self->{_out_buffer};
231             }
232 0           $self->{_transport}->write( $self->{_out_buffer} );
233 0           $self->{_transport}->flush();
234 0           $self->{_out_buffer} = '';
235              
236             #print STDERR Dumper $self;
237             }
238              
239             sub isOpen {
240 0     0 0   shift->{_transport}->isOpen(@_);
241             }
242              
243             sub readAll {
244 0     0 0   my $self = shift;
245             print STDERR ">>> readAll $_[0] bytes\n"
246 0 0         if $self->{_debug} > 1;
247 0           my $ret = $self->{_transport}->readAll(@_);
248 0           return $ret;
249             }
250              
251             1;
252              
253             #ABSTRACT: Thrift Transport allowing Kerberos auth/encryption through GSSAPI
254              
255             __END__