File Coverage

blib/lib/Growl/GNTP.pm
Criterion Covered Total %
statement 45 293 15.3
branch 1 138 0.7
condition 11 79 13.9
subroutine 9 28 32.1
pod 5 5 100.0
total 71 543 13.0


line stmt bran cond sub pod time code
1             package Growl::GNTP;
2              
3 2     2   55129 use strict;
  2         5  
  2         96  
4 2     2   10 use warnings;
  2         4  
  2         159  
5 2     2   3125 use IO::Socket::INET;
  2         91947  
  2         15  
6 2     2   2998 use Data::UUID;
  2         12304  
  2         144  
7 2     2   1986 use Crypt::CBC;
  2         11561  
  2         78  
8 2     2   19 use Digest::MD5 qw/md5_hex/;
  2         3  
  2         119  
9 2     2   2640 use Digest::SHA qw/sha1_hex sha256_hex/;
  2         9339  
  2         9878  
10             our $VERSION = '0.20';
11              
12             sub new {
13 1     1 1 16 my $class = shift;
14 1         6 my %args = @_;
15 1   50     10 $args{Proto} ||= 'tcp';
16 1   50     8 $args{PeerHost} ||= 'localhost';
17 1   50     9 $args{PeerPort} ||= 23053;
18 1   50     7 $args{Timeout} ||= 5;
19 1   50     5 $args{AppName} ||= 'Growl::GNTP';
20 1   50     8 $args{AppIcon} ||= '';
21 1   50     8 $args{Password} ||= '';
22 1   50     8 $args{PasswordHashAlgorithm} ||= 'MD5';
23 1   50     7 $args{EncryptAlgorithm} ||= 'NONE';
24 1   50     7 $args{Debug} ||= 0;
25 1         2 $args{Callbacks} = [];
26 1         47 srand();
27 1         15 bless {%args}, $class;
28             }
29              
30             sub register {
31 1     1 1 11 my $self = shift;
32 1   50     5 my $notifications = shift || [];
33              
34 1         7 my $AppName = $self->{AppName};
35 1         4 $AppName =~ s!\r\n!\n!;
36 1         3 my $AppIcon = $self->{AppIcon};
37 1         2 $AppIcon =~ s!\r\n!\n!;
38 1         3 my $count = scalar @$notifications;
39              
40 1         15 my $sock = IO::Socket::INET->new(
41             PeerAddr => $self->{PeerHost},
42             PeerPort => $self->{PeerPort},
43             Proto => $self->{Proto},
44             Timeout => $self->{Timeout},
45             );
46 1 50       6545 die $@ unless $sock;
47              
48 0           my $identifier;
49 0 0         if (-f $AppIcon) {
50 0           open my $f, "<:raw", $AppIcon;
51 0           $identifier = do { local $/; <$f> };
  0            
  0            
52 0           close $f;
53 0           $AppIcon = "x-growl-resource://" . Digest::MD5::md5_hex(Digest::MD5->new->add($identifier)->digest);
54             }
55              
56 0           my $form = <
57             Application-Name: $AppName
58             Application-Icon: $AppIcon
59             Notifications-Count: $count
60              
61             EOF
62 0           $form =~ s!\n!\r\n!g;
63              
64 0           $count = 0;
65 0           for my $notification ( @{$notifications} ) {
  0            
66 0           $count++;
67 0   0       my %data = (
      0        
      0        
      0        
68             Name => $notification->{Name} || "Growl::GNTP Notify$count",
69             DisplayName => $notification->{DisplayName}
70             || $notification->{Name} || "Growl::GNTP Notify$count",
71             Enabled => _translate_bool($notification->{Enabled} || 'True'),
72             Icon => $notification->{Icon} || '', # will default to Application-Icon if not specified.
73             );
74 0           $data{$_} =~ s!\r\n!\n! for ( keys %data );
75              
76 0           my $subform .= <
77             Notification-Name: \$(Name)
78             Notification-Display-Name: \$(DisplayName)
79             Notification-Enabled: \$(Enabled)
80             Notification-Icon: \$(Icon)
81              
82             EOF
83 0           $subform =~ s!\n!\r\n!g;
84 0           $subform =~ s/\$\((\w+)\)/$data{$1}/ge;
  0            
85 0           $form .= $subform;
86             }
87 0 0         if ($identifier) {
88 0           $form.=sprintf("Identifier: %s\r\r\n",substr($AppIcon, 19));
89 0           $form.=sprintf("Length: %d\r\r\n\r\r\n",length $identifier);
90 0           $form =~ s!\r\r\n!\r\n!g;
91 0           $form .= $identifier;
92 0           $form .= "\r\n\r\n";
93             }
94              
95 0 0         print $form if $self->{Debug};
96 0           $form = _gen_header($self, 'REGISTER', $form);
97 0           $sock->send($form);
98              
99 0           my $ret = <$sock>;
100 0 0         $ret = $1 if $ret =~ /^GNTP\/1\.0 -?(\w+)/;
101 0 0         print "$_\n" if $self->{Debug};
102              
103 0           my $description = 'failed to register';
104 0 0         if ($ret ne 'OK') {
105 0           while (<$sock>) {
106 0           $_ =~ s!\r\n!!g;
107 0 0         print "$_\n" if $self->{Debug};
108 0 0         $description = $1 if $_ =~ /^Error-Description:\s*(.*)$/;
109 0 0         last if length($_) == 0;
110             }
111             }
112 0           close $sock;
113              
114 0 0         die $description if $ret ne 'OK';
115             }
116              
117             sub notify {
118 0     0 1   my ( $self, %args ) = @_;
119 0   0       my %data = (
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
120             AppName => $self->{AppName},
121             Name => $args{Name} || $args{Event} || '',
122             Title => $args{Title} || '',
123             Message => $args{Message} || '',#optional
124             Icon => $args{Icon} || '', #optional
125             ID => $args{ID} || '', # optional
126             CoalescingID => $args{CoalescingID} || '', # optional
127             Priority => _translate_int($args{Priority} || 0), #optional
128             Sticky => _translate_bool($args{Sticky} || 'False'), #optional
129             CallbackContext => $args{CallbackContext} || '',#optional
130             CallbackContextType => $args{CallbackContextType} || '',#optional, required if CallbackContext
131             CallbackTarget => $args{CallbackTarget} || '', #optional exclusive of CallbackContext[-Type] #!# for now, needs Context pair until GfW v2.0.0.20
132             CallbackFunction => $args{CallbackFunction} || {}, #optional
133             Custom => $args{Custom} || '', # optional
134             );
135 0           $data{$_} =~ s!\r\n!\n! for ( keys %data );
136              
137 0           my $identifier;
138 0 0         if (-f $data{Icon}) {
139 0           open my $f, "<:raw", $data{Icon};
140 0           $identifier = do { local $/; <$f> };
  0            
  0            
141 0           close $f;
142 0           $data{Icon} = "x-growl-resource://" . Digest::MD5::md5_hex(Digest::MD5->new->add($identifier)->digest);
143             }
144              
145             # once GfW v2.0.0.20, this CallbackTarget can be removed.
146 0 0         if ($data{CallbackTarget}) {
147 0   0       $data{CallbackContext} = $data{CallbackContext} || 'TARGET';
148 0   0       $data{CallbackContextType} = $data{CallbackContextType} || 'TARGET';
149             }
150              
151 0           my $sock = IO::Socket::INET->new(
152             PeerAddr => $self->{PeerHost},
153             PeerPort => $self->{PeerPort},
154             Proto => $self->{Proto},
155             Timeout => $self->{Timeout},
156             );
157 0 0         die $@ unless $sock;
158              
159 0           my $form;
160 0           $form.=sprintf("Application-Name: %s\r\r\n",$data{AppName});
161 0           $form.=sprintf("Notification-Name: %s\r\r\n",$data{Name});
162 0           $form.=sprintf("Notification-Title: %s\r\r\n",$data{Title});
163 0 0         $form.=sprintf("Notification-ID: %s\r\r\n",$data{ID}) if $data{ID};
164 0 0         $form.=sprintf("Notification-Priority: %s\r\r\n",$data{Priority}) if $data{Priority};
165 0 0         $form.=sprintf("Notification-Text: %s\r\r\n",$data{Message}) if $data{Message};
166 0 0         $form.=sprintf("Notification-Sticky: %s\r\r\n",$data{Sticky}) if $data{Sticky};
167 0 0         $form.=sprintf("Notification-Icon: %s\r\r\n",$data{Icon}) if $data{Icon};
168 0 0         $form.=sprintf("Notification-Coalescing-ID: %s\r\r\n",$data{CoalescingID}) if $data{CoalescingID};
169 0 0         if ($data{CallbackContext}) {
170 0           $form.=sprintf("Notification-Callback-Context: %s\r\r\n",$data{CallbackContext});
171 0           $form.=sprintf("Notification-Callback-Context-Type: %s\r\r\n",$data{CallbackContextType});
172             }
173 0 0         if ($data{CallbackTarget}) { # BOTH method are provided here for GfW compatability.
174 0           $form.=sprintf("Notification-Callback-Context-Target: %s\r\r\n",$data{CallbackTarget});
175 0           $form.=sprintf("Notification-Callback-Target: %s\r\r\n",$data{CallbackTarget});
176             }
177 0 0         if (ref($data{Custom}) eq 'HASH') {
178 0           foreach my $header (sort keys %{$data{Custom}}){
  0            
179 0           $form.=sprintf("X-%s: %s\r\r\n",$header,$data{Custom}{$header});
180             }
181             }
182              
183 0 0         if ($identifier) {
184 0           $form .= "\r\r\n";
185 0           $form.=sprintf("Identifier: %s\r\r\n",substr($data{Icon}, 19));
186 0           $form.=sprintf("Length: %d\r\r\n\r\r\n",length $identifier);
187 0           $form =~ s!\r\r\n!\r\n!g;
188 0           $form .= $identifier;
189 0           $form .= "\r\n";
190             } else {
191 0           $form =~ s!\r\r\n!\r\n!g;
192             }
193 0           $form .= "\r\n";
194 0 0         print $form if $self->{Debug};
195              
196 0           $form = _gen_header($self, 'NOTIFY', $form);
197 0           $sock->send($form);
198              
199 0           my $ret = <$sock>;
200 0 0         $ret = $1 if $ret =~ /^GNTP\/1\.0 -?(\w+)/;
201 0 0         print "$_\n" if $self->{Debug};
202              
203 0           my $description = 'failed to notify';
204 0 0         if ($ret ne 'OK') {
205 0           while (<$sock>) {
206 0           $_ =~ s!\r\n!!g;
207 0 0         print "$_\n" if $self->{Debug};
208 0 0         $description = $1 if $_ =~ /^Error-Description:\s*(.*)$/;
209 0 0         last if length($_) == 0;
210             }
211             }
212 0           close $sock;
213              
214 0 0         die $description if $ret ne 'OK';
215             }
216              
217             sub subscribe {
218 0     0 1   my ( $self, %args ) = @_;
219 0           chomp(my $hostname = `hostname`);
220 0   0       my %data = (
      0        
      0        
221             ID => $args{ID} || Data::UUID->new->create_str,
222             Name => $args{Name} || $hostname,
223             Port => $args{Port} || 23053,
224             );
225 0           $data{$_} =~ s!\r\n!\n! for ( keys %data );
226 0   0       my $password = $args{Password} || '';
227 0   0       my $callback = $args{CallbackFunction} || '';
228              
229 0           my $sock = IO::Socket::INET->new(
230             PeerAddr => $self->{PeerHost},
231             PeerPort => $self->{PeerPort},
232             Proto => $self->{Proto},
233             Timeout => $self->{Timeout},
234             );
235 0 0         die $@ unless $sock;
236              
237 0           my $form = <
238             Subscriber-ID: \$(ID)
239             Subscriber-Name: \$(Name)
240             Subscriber-Port: \$(Port)
241              
242             EOF
243 0           $form =~ s!\r?\n!\r\n!g;
244 0           $form =~ s/\$\((\w+)\)/$data{$1}/ge;
  0            
245              
246 0           $form = _gen_header($self, 'SUBSCRIBE', $form);
247 0           $sock->send($form);
248              
249 0           my $ret = <$sock>;
250 0 0         $ret = $1 if $ret =~ /^GNTP\/1\.0 -?(\w+)/;
251 0 0         print "$_\n" if $self->{Debug};
252              
253 0           my $description = 'failed to register';
254 0 0         if ($ret ne 'OK') {
255 0           while (<$sock>) {
256 0           $_ =~ s!\r\n!!g;
257 0 0         print "$_\n" if $self->{Debug};
258 0 0         $description = $1 if $_ =~ /^Error-Description:\s*(.*)$/;
259 0 0         last if length($_) == 0;
260             }
261 0 0         die $description if $ret ne 'OK';
262             }
263              
264 0           $sock = IO::Socket::INET->new(
265             LocalPort => $data{Port},
266             Proto => 'tcp',
267             Listen => 10,
268             Timeout => $self->{Timeout},
269             );
270 0 0         die $@ unless $sock;
271              
272 0           $description = 'failed to subscribe';
273 0           while (1) {
274 0           my $client = $sock->accept();
275 0           my ($Title, $Message) = ('', '');
276 0           while (<$client>){
277 0           $_ =~ s!\r\n!!g;
278 0 0         print "$_\n" if $self->{Debug};
279 0 0         $ret = $1 if $_ =~ /^GNTP\/1\.0 -?(\w+)/;
280 0 0         $description = $1 if $_ =~ /^Error-Description:\s*(.*)$/;
281 0 0         $Title = $1 if $_ =~ /^Notification-Title: (.*)\r\n/;
282 0 0         $Message = $1 if $_ =~ /^Notification-Text: (.*)\r\n/;
283             # TODO
284             # handling more GNTP protocols.
285             # currently, can't treat multiline header which include LF.
286             ## hrmmm...
287 0 0         last if length($_) == 0;
288             }
289 0           $client->close();
290              
291 0 0 0       if ($Title && ref($callback) eq 'CODE') {
292 0           $callback->($Title, $Message);
293             }
294             }
295              
296 0 0         die $description if $ret ne 'OK';
297             }
298              
299             sub wait {
300 0     0 1   my $self = shift;
301 0   0       my $waitall = shift || 1;
302              
303 0           my @callbacks = @{$self->{Callbacks}};
  0            
304 0           my @old = @callbacks;
305 0           my $bits = "";
306 0           while (@callbacks) {
307 0           vec($bits, fileno($_->{Socket}), 1) = 1 for @callbacks;
308 0 0         next unless select($bits, undef, undef, 0.1);
309 0           for (my $i = 0; $i < @callbacks; $i++) {
310 0           my $callback = $callbacks[$i];
311 0           my $sock = $callback->{Socket};
312 0 0         if (vec($bits, fileno($sock), 1)) {
313 0           my ($result, $type, $context, $id, $timestamp) = ('', '', '','','');
314 0           while (<$sock>) {
315 0           $_ =~ s!\r\n!!g;
316 0 0         print "$_\n" if $self->{Debug};
317 0 0         $id = $1 if $_ =~ /^Notification-ID: (.*)$/;
318 0 0         $timestamp = $1 if $_ =~ /^Notification-Callback-Timestamp: (.*)$/;
319 0 0         $result = $1 if $_ =~ /^Notification-Callback-Result: (.*)$/;
320 0 0         $context = $1 if $_ =~ /^Notification-Callback-Context: (.*)$/;
321 0 0         $type = $1 if $_ =~ /^Notification-Callback-Context-Type: (.*)$/;
322 0 0         last if length($_) == 0;
323             }
324 0 0         if (ref($callback->{Function}) eq 'CODE') {
325 0           $callback->{Function}->($result, $type, $context,$id,$timestamp);
326             }
327 0           splice(@callbacks, $i, 1);
328             }
329             }
330 0 0         last unless $waitall;
331             };
332              
333 0           for (my $i = 0; $i < @{$self->{Callbacks}}; ++$i) {
  0            
334 0 0         if (grep { $_->{Socket} eq $self->{Callbacks}[$i]->{Socket} } @old) {
  0            
335 0           splice(@{$self->{Callbacks}}, $i--, 1);
  0            
336             }
337             }
338 0           1;
339             }
340              
341             sub _translate_int {
342 0     0     return 0 + shift;
343             }
344              
345             sub _translate_bool {
346 0     0     my $value = shift;
347 0 0         return 'True' if $value =~ /^([Tt]rue|[Yy]es)$/;
348 0 0         return 'False' if $value =~ /^([Ff]alse|[Nn]o)$/;
349 0 0         return 'True' if $value;
350 0           return 'False';
351             }
352              
353             sub _gen_header {
354 0     0     my ($ctx, $method, $form) = @_;
355              
356 0 0         if ($ctx->{Password}) {
357 0           my ($hash, $salt) = _gen_hash($ctx);
358 0           my $crypt = _gen_encrypt($ctx, $salt, \$form);
359 0 0         if ($crypt eq 'NONE') {
360 0           $form = "GNTP/1.0 $method NONE $hash\r\n$form\r\n";
361             } else {
362 0           $form = "GNTP/1.0 $method $crypt $hash\r\n$form\r\n\r\n";
363             }
364             } else {
365 0           $form = "GNTP/1.0 $method NONE\r\n$form\r\n";
366             }
367 0           return $form;
368             }
369              
370             sub _gen_salt {
371 0     0     my $count = shift;
372 0           my @salt = ( '.', '/', 0 .. 9, 'A' .. 'Z', 'a' .. 'z' );
373 0           my $salt;
374 0           $salt .= (@salt)[rand @salt] for 1..$count;
375 0           return $salt;
376             }
377              
378             sub _gen_hash {
379 0     0     my $ctx = shift;
380 0           my $hash_algorithm = $ctx->{PasswordHashAlgorithm};
381 0           my $password = $ctx->{Password};
382 0 0         return 'NONE' if $hash_algorithm eq 'NONE';
383              
384 0           my $salt = _gen_salt(8);
385 0           my $salthex = uc unpack("H*", $salt);
386              
387             my %hashroll = (
388 0     0     'MD5' => sub { my ($password, $salt) = @_; return uc Digest::MD5::md5_hex(Digest::MD5->new->add($password)->add($salt)->digest); },
  0            
389 0     0     'SHA1' => sub { my ($password, $salt) = @_; return uc Digest::SHA::sha1_hex(Digest::SHA->new(1)->add($password)->add($salt)->digest); },
  0            
390 0     0     'SHA256' => sub { my ($password, $salt) = @_; return uc Digest::SHA::sha256_hex(Digest::SHA->new(256)->add($password)->add($salt)->digest); },
  0            
391 0           );
392 0           my $key = $hashroll{$hash_algorithm}->($password, $salt);
393 0           return "$hash_algorithm:$key.$salthex", $salt;
394             }
395              
396             sub _gen_encrypt {
397 0     0     my ($ctx, $salt, $data) = @_;
398 0           my $hash_algorithm = $ctx->{PasswordHashAlgorithm};
399 0           my $crypt_algorithm = $ctx->{EncryptAlgorithm};
400 0           my $password = $ctx->{Password};
401 0 0         return 'NONE' if $crypt_algorithm eq 'NONE';
402              
403             my %hashroll = (
404 0     0     'MD5' => sub { my ($password, $salt) = @_; return Digest::MD5->new->add($password)->add($salt)->digest },
  0            
405 0     0     'SHA1' => sub { my ($password, $salt) = @_; return Digest::SHA->new(1)->add($password)->add($salt)->digest },
  0            
406 0     0     'SHA256' => sub { my ($password, $salt) = @_; return Digest::SHA->new(256)->add($password)->add($salt)->digest },
  0            
407 0           );
408 0           my $key = $hashroll{$hash_algorithm}->($password, $salt);
409              
410             my %cryptroll = (
411             'AES' => sub {
412 0     0     my ($data, $key) = @_;
413 0           my $iv = Crypt::CBC->random_bytes(16);
414 0           my $cbc = Crypt::CBC->new(
415             -key => substr($key, 0, 24),
416             -iv => $iv,
417             -keysize => 24,
418             -header => 'none',
419             -literal_key => 1,
420             -padding => 'standard',
421             -cipher => 'Crypt::OpenSSL::AES',
422             );
423 0           return $cbc->encrypt($data), uc unpack("H*", $iv);
424             },
425             'DES' => sub {
426 0     0     my ($data, $key) = @_;
427 0           my $iv = Crypt::CBC->random_bytes(8);
428 0           my $cbc = Crypt::CBC->new(
429             -key => substr($key, 0, 8),
430             -iv => $iv,
431             -header => 'none',
432             -literal_key => 1,
433             -padding => 'standard',
434             -cipher => 'DES',
435             );
436 0           return $cbc->encrypt($data), uc unpack("H*", $iv);
437             },
438             '3DES' => sub {
439 0     0     my ($data, $key) = @_;
440 0           my $iv = Crypt::CBC->random_bytes(8);
441 0 0         $key = $key.substr($key,0,24-length($key)) if length($key) < 24;
442 0           my $cbc = Crypt::CBC->new(
443             -key => substr($key, 0, 24),
444             -iv => $iv,
445             -header => 'none',
446             -literal_key => 1,
447             -padding => 'standard',
448             -cipher => 'DES_EDE3',
449             );
450 0           return $cbc->encrypt($data), uc unpack("H*", $iv);
451             },
452 0           );
453 0           ($$data, my $hash) = $cryptroll{$crypt_algorithm}->($$data, $key);
454 0           return "$crypt_algorithm:$hash";
455             }
456              
457             sub _debug {
458 0     0     my ($name, $data) = @_;
459 0           open my $f, ">", $name;
460 0           binmode $f;
461 0           print $f $data;
462 0           close $f;
463             }
464              
465             1;
466             __END__