File Coverage

blib/lib/Shadowd/Connector.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Shadowd::Connector;
2              
3 1     1   15333 use 5.010;
  1         4  
  1         95  
4 1     1   6 use strict;
  1         2  
  1         29  
5              
6 1     1   582 use JSON;
  1         12748  
  1         4  
7 1     1   639 use Switch;
  1         25103  
  1         5  
8 1     1   80213 use Config::IniFiles;
  0            
  0            
9             use IO::Socket;
10             use IO::Socket::SSL;
11             use Crypt::Mac::HMAC qw(hmac_hex);
12             use Attribute::Abstract;
13             use POSIX qw(strftime);
14              
15             use constant {
16             SHADOWD_CONNECTOR_VERSION => '1.0.2-perl',
17             SHADOWD_CONNECTOR_CONFIG => '/etc/shadowd/connectors.ini',
18             SHADOWD_CONNECTOR_CONFIG_SECTION => 'shadowd_perl',
19             SHADOWD_LOG => '/var/log/shadowd.log',
20             STATUS_OK => 1,
21             STATUS_BAD_REQUEST => 2,
22             STATUS_BAD_SIGNATURE => 3,
23             STATUS_BAD_JSON => 4,
24             STATUS_ATTACK => 5
25             };
26              
27             =head1 NAME
28              
29             Shadowd::Connector - Shadow Daemon Connector (Base)
30              
31             =head1 VERSION
32              
33             Version 1.0.3
34              
35             =cut
36              
37             our $VERSION = '1.0.3';
38              
39             =head1 SYNOPSIS
40              
41             B is a collection of tools to I, I and I I on I.
42             Technically speaking, Shadow Daemon is a B that intercepts requests and filters out malicious parameters.
43             It is a modular system that separates web application, analysis and interface to increase security, flexibility and expandability.
44              
45             I is the base class to connect Perl applications with the Shadow Daemon background server. It is not possible
46             to use this module directly, because there are abstract methods that have to be implemented.
47              
48             =cut
49              
50             =head1 METHODS
51              
52             =head2 new()
53              
54             This method is a simple constructor for an object oriented interface.
55              
56             =cut
57              
58             sub new {
59             my ($class) = @_;
60             my $self = {};
61              
62             bless $self, $class;
63             return $self;
64             }
65              
66             =head2 get_client_ip()
67              
68             This is an abstract method that has to be implemented by a subclass. It has to return the IP address of the client.
69              
70             =cut
71              
72             sub get_client_ip: Abstract;
73              
74             =head2 get_caller()
75              
76             This is an abstract method that has to be implemented by a subclass. It has to return the caller, i.e. the requested resource.
77              
78             =cut
79              
80             sub get_caller: Abstract;
81              
82             =head2 gather_input()
83              
84             This is an abstract method that has to be implemented by a subclass. It has to save the user input in the class attribute I<_input>.
85              
86             =cut
87              
88             sub gather_input: Abstract;
89              
90             =head2 defuse_input($threats)
91              
92             This is an abstract method that has to be implemented by a subclass. It has to remove threats from the user input.
93              
94             =cut
95              
96             sub defuse_input: Abstract;
97              
98             =head2 error()
99              
100             This is an abstract method that has to be implemented by a subclass. It has to display an error message.
101              
102             =cut
103              
104             sub error: Abstract;
105              
106             =head2 init_config()
107              
108             This method initializes and loads the configuration.
109              
110             =cut
111              
112             sub init_config {
113             my ($self) = @_;
114              
115             if (defined $ENV{'SHADOWD_CONNECTOR_CONFIG'}) {
116             $self->{'_config_file'} = $ENV{'SHADOWD_CONNECTOR_CONFIG'};
117             } else {
118             $self->{'_config_file'} = SHADOWD_CONNECTOR_CONFIG;
119             }
120              
121             $self->{'_config'} = Config::IniFiles->new(-file => $self->{'_config_file'});
122              
123             if (!$self->{'_config'}) {
124             die('config error');
125             }
126              
127             if (defined $ENV{'SHADOWD_CONNECTOR_CONFIG_SECTION'}) {
128             $self->{'_config_section'} = $ENV{'SHADOWD_CONNECTOR_CONFIG_SECTION'};
129             } else {
130             $self->{'_config_section'} = SHADOWD_CONNECTOR_CONFIG_SECTION;
131             }
132             }
133              
134             =head2 get_config($key, $required, $default)
135              
136             This method returns values from the configuration.
137              
138             =cut
139              
140             sub get_config {
141             my ($self, $key, $required, $default) = @_;
142              
143             if (!$self->{'_config'}->exists($self->{'_config_section'}, $key)) {
144             if ($required) {
145             die($key . ' in config missing');
146             } else {
147             return $default;
148             }
149             } else {
150             return $self->{'_config'}->val($self->{'_config_section'}, $key);
151             }
152             }
153              
154             =head2 get_input()
155              
156             This method returns the user input that is brought together by I.
157              
158             =cut
159              
160             sub get_input {
161             my ($self) = @_;
162              
163             return $self->{'_input'}
164             }
165              
166             =head2 remove_ignored($file)
167              
168             The method removes user input that should be ignored from the class attribute I<_input>.
169              
170             =cut
171              
172             sub remove_ignored {
173             my ($self, $file) = @_;
174              
175             local $/ = undef;
176             open my $handler, $file or die('could not open ignore file: ' . $!);
177             binmode $handler;
178              
179             my $content = <$handler>;
180             my $json = decode_json($content);
181              
182             foreach my $entry (@$json) {
183             if (!defined $entry->{'path'} && defined $entry->{'caller'}) {
184             if ($self->{'_caller'} eq $entry->{'caller'}) {
185             $self->{'_input'} = {};
186              
187             last;
188             }
189             } else {
190             if (defined $entry->{'caller'}) {
191             if ($self->{'_caller'} ne $entry->{'caller'}) {
192             next;
193             }
194             }
195              
196             if (defined $entry->{'path'}) {
197             delete $self->{'_input'}->{$entry->{'path'}};
198             }
199             }
200             }
201              
202             close $handler;
203             }
204              
205             =head2 send_input($host, $port, $profile, $key, $ssl)
206              
207             This method sends the user input to the background server and return the parsed response.
208              
209             =cut
210              
211             sub send_input {
212             my ($self, $host, $port, $profile, $key, $ssl) = @_;
213              
214             my $connection;
215              
216             if ($ssl) {
217             $connection = IO::Socket::SSL->new(
218             PeerHost => $host,
219             PeerPort => $port,
220             SSL_verify_mode => SSL_VERIFY_PEER,
221             SSL_ca_file => $ssl
222             ) or die('network error (ssl): ' . $!);
223             } else {
224             $connection = IO::Socket::INET->new(
225             PeerAddr => $host,
226             PeerPort => $port
227             ) or die('network error: ' . $!);
228             }
229              
230             $connection->autoflush(1);
231              
232             my %input_data = (
233             'version' => SHADOWD_CONNECTOR_VERSION,
234             'client_ip' => $self->get_client_ip,
235             'caller' => $self->get_caller,
236             'input' => $self->get_input
237             );
238              
239             my $json = encode_json(\%input_data);
240             print $connection $profile . "\n" . $self->sign($key, $json) . "\n" . $json . "\n";
241              
242             my $output = <$connection>;
243              
244             close $connection;
245              
246             return $self->parse_output($output);
247             }
248              
249             =head2 parse_output($output)
250              
251             This method parses the response of the background server.
252              
253             =cut
254              
255             sub parse_output {
256             my ($self, $output) = @_;
257              
258             my $output_data = decode_json($output);
259              
260             switch ($output_data->{'status'}) {
261             case STATUS_OK { return 0; }
262             case STATUS_BAD_REQUEST { die('bad request'); }
263             case STATUS_BAD_SIGNATURE { die('bad signature'); }
264             case STATUS_BAD_JSON { die('bad json'); }
265             case STATUS_ATTACK { return $output_data->{'threats'}; }
266             else { die('processing error'); }
267             }
268             }
269              
270             =head2 sign($key, $json)
271              
272             This method signs the input with a secret key to authenticate requests without having to send the password.
273              
274             =cut
275              
276             sub sign {
277             my ($self, $key, $json) = @_;
278              
279             return hmac_hex('SHA256', $key, $json);
280             }
281              
282             =head2 log($message)
283              
284             This method writes messages to a log file.
285              
286             =cut
287              
288             sub log {
289             my ($self, $message) = @_;
290              
291             my $file = $self->get_config('log', 0, SHADOWD_LOG);
292             open my $handler, '>>' . $file or die('could not open log file: ' . $!);
293              
294             my $datetime = strftime('%Y-%m-%d %H:%M:%S', localtime);
295             print $handler $datetime . "\t" . $message;
296              
297             close $handler;
298             }
299              
300             =head2 escape_key($key)
301              
302             This method escapes keys, i.e. single elements of a path.
303              
304             =cut
305              
306             sub escape_key {
307             my ($self, $key) = @_;
308              
309             $key =~ s/\\/\\\\/g;
310             $key =~ s/\|/\\|/g;
311              
312             return $key;
313             }
314              
315             =head2 unescape_key($key)
316              
317             This method unescapes keys, i.e. single elements of a path.
318              
319             =cut
320              
321             sub unescape_key {
322             my ($self, $key) = @_;
323              
324             $key =~ s/\\\\/\\/g;
325             $key =~ s/\\\|/|/g;
326              
327             return $key;
328             }
329              
330             =head2 split_path($path)
331              
332             This method splits a path into keys.
333              
334             =cut
335              
336             sub split_path {
337             my ($self, $path) = @_;
338              
339             return split(/\\.(*SKIP)(*FAIL)|\|/s, $path);
340             }
341              
342             =head2 start()
343              
344             This method connects the different components of the module and starts the complete protection process.
345              
346             =cut
347              
348             sub start {
349             my ($self) = @_;
350              
351             eval {
352             $self->init_config;
353              
354             $self->gather_input;
355              
356             my $ignored = $self->get_config('ignore');
357             if ($ignored) {
358             $self->remove_ignored($ignored);
359             }
360              
361             my $threats = $self->send_input(
362             $self->get_config('host', 0, '127.0.0.1'),
363             $self->get_config('port', 0, '9115'),
364             $self->get_config('profile', 1),
365             $self->get_config('key', 1),
366             $self->get_config('ssl')
367             );
368              
369             if (!$self->get_config('observe') && $threats) {
370             $self->defuse_input($threats);
371             }
372              
373             if ($self->get_config('debug') && $threats) {
374             $self->log('shadowd: removed threat from client: ' . $self->get_client_ip . "\n");
375             }
376             };
377              
378             if ($@) {
379             if ($self->get_config('debug')) {
380             $self->log($@);
381             }
382              
383             unless ($self->get_config('observe')) {
384             $self->error;
385              
386             return undef;
387             }
388             }
389              
390             return 1;
391             }
392              
393             =head1 AUTHOR
394              
395             Hendrik Buchwald, C<< >>
396              
397             =head1 BUGS
398              
399             Please report any bugs or feature requests to C, or through the web interface at
400             L. I will be notified, and then you'll automatically
401             be notified of progress on your bug as I make changes.
402              
403             It is also possible to report bugs via Github at L.
404              
405             =head1 SUPPORT
406              
407             You can find documentation for this module with the perldoc command.
408              
409             perldoc Shadowd::Connector
410              
411              
412             You can also look for information at:
413              
414             =over 4
415              
416             =item * RT: CPAN's request tracker (report bugs here)
417              
418             L
419              
420             =item * AnnoCPAN: Annotated CPAN documentation
421              
422             L
423              
424             =item * CPAN Ratings
425              
426             L
427              
428             =item * Search CPAN
429              
430             L
431              
432             =back
433              
434             =head1 LICENSE AND COPYRIGHT
435              
436             Shadow Daemon -- Web Application Firewall
437              
438             Copyright (C) 2014-2015 Hendrik Buchwald C<< >>
439              
440             This file is part of Shadow Daemon. Shadow Daemon is free software: you can
441             redistribute it and/or modify it under the terms of the GNU General Public
442             License as published by the Free Software Foundation, version 2.
443              
444             This program is distributed in the hope that it will be useful, but WITHOUT
445             ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
446             FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
447             details.
448              
449             You should have received a copy of the GNU General Public License
450             along with this program. If not, see L.
451              
452             =cut
453              
454             1; # End of Shadowd::Connector