File Coverage

blib/lib/MikroTik/API.pm
Criterion Covered Total %
statement 39 281 13.8
branch 0 130 0.0
condition 0 24 0.0
subroutine 14 36 38.8
pod 9 10 90.0
total 62 481 12.8


line stmt bran cond sub pod time code
1             package MikroTik::API;
2              
3 1     1   56867 use 5.006;
  1         4  
4 1     1   5 use strict;
  1         1  
  1         21  
5 1     1   3 use warnings FATAL => 'all';
  1         2  
  1         58  
6              
7             =head1 NAME
8              
9             MikroTik::API - Client to MikroTik RouterOS API
10              
11             =head1 VERSION
12              
13             Version 2.0.1
14              
15             B Dependencies change with version 2.0.0. MikroTik::API now relies on Moo instead of Moose. Please be sure, dependencies are met before upgrading.
16              
17             =cut
18              
19             our $VERSION = '2.0.1';
20              
21              
22             =head1 SYNOPSIS
23              
24             use MikroTik::API;
25              
26             my $api = MikroTik::API->new({
27             host => 'mikrotik.example.org',
28             username => 'whoami',
29             password => 'SECRET',
30             use_ssl => 1,
31             });
32              
33             my ( $ret_get_identity, @aoh_identity ) = $api->query( '/system/identity/print', {}, {} );
34             print "Name of router: $aoh_identity[0]->{name}\n";
35              
36             $api->logout();
37              
38             =head1 DESCRIPTION
39              
40             =cut
41              
42 1     1   6 use Carp qw(croak);
  1         2  
  1         47  
43 1     1   6 use Digest::MD5;
  1         1  
  1         27  
44 1     1   511 use IO::Socket::IP;
  1         29693  
  1         4  
45 1     1   1099 use IO::Socket::SSL;
  1         38924  
  1         10  
46 1     1   628 use Moo;
  1         9360  
  1         5  
47 1     1   1902 use MooX::Types::MooseLike::Base qw(Bool CodeRef Int Str);
  1         5408  
  1         75  
48 1     1   450 use Time::Out qw{ timeout };
  1         498  
  1         48  
49 1     1   448 use Try::Tiny;
  1         1064  
  1         44  
50 1     1   498 use Type::Tiny;
  1         14124  
  1         34  
51 1     1   427 use namespace::autoclean;
  1         9464  
  1         4  
52              
53              
54             =head1 PUBLIC METHODS
55              
56             =head2 new( \%config )
57              
58             my $api = MikroTik::API->new({
59             host => 'mikrotik.example.org',
60             username => 'whoami',
61             password => 'SECRET',
62             autoconnect => 1, # optional (set to 0 if you do not want to connect during construction, default: 1)
63             use_ssl => 1, # optional (0 for non ssl / 1 for ssl)
64             port => 8729, # optonal (needed if you use another port then 8728 for non-ssl or 8729 for ssl)
65             debug => 0, # optional (set beween 0 (none) and 5 (most) for debug messages)
66             timeout => 3, # optional (timeout after 3 seconds during connect)
67             probe_before_talk => 3, # optional (probe connection before each actual command)
68             reconnect_after_failed_probe => 1, # optional (reconnect if probe failed)
69              
70             });
71              
72             =cut
73              
74             sub BUILD {
75 0     0 0 0 my ($self) = @_;
76 0 0 0     0 if ( $self->get_autoconnect() && $self->get_host() ) {
77 0         0 $self->connect();
78 0 0 0     0 if ( $self->get_username() && defined( $self->get_password() ) ) {
79 0         0 $self->login();
80             }
81             }
82 0         0 return $self;
83             }
84              
85             =head2 $api->connect()
86              
87             Connect happens on construction if you provide host address
88              
89             my $api = MikroTik::API->new();
90              
91             $api->set_host('mikrotik.example.org');
92             $api->set_port(1234);
93             $api->set_use_ssl(1);
94              
95             $api->connect();
96              
97             =cut
98              
99             sub connect {
100 0     0 1 0 my ( $self ) = @_;
101              
102 0 0       0 if ( ! $self->get_host() ) {
103 0         0 croak 'host must be set before connect()'
104             }
105              
106 0 0       0 if ( $self->get_use_ssl ) {
107             my $socket
108             = try {
109 0     0   0 IO::Socket::SSL->new(
110             PeerHost => $self->get_host,
111             PeerPort => $self->get_port,
112             Proto => 'tcp',
113             SSL_cipher_list => 'HIGH',
114             SSL_verify_mode => $self->get_ssl_verify(),
115             Timeout => $self->get_timeout,
116             );
117             }
118             catch {
119 0     0   0 croak sprintf
120             'failed connect %s:%s or ssl handshake (%s: %s)',
121             $self->get_host,
122             $self->get_port,
123             $_,
124             IO::Socket::SSL::errstr();
125 0         0 };
126             # obsolete?
127 0 0       0 $socket
128             or croak sprintf
129             'failed connect %s:%s or ssl handshake (%s: %s)',
130             $self->get_host,
131             $self->get_port,
132             $!,
133             IO::Socket::SSL::errstr();
134 0         0 $self->set_socket($socket);
135             }
136             else {
137 0   0     0 $self->set_socket(
138             IO::Socket::IP->new(
139             PeerHost => $self->get_host,
140             PeerPort => $self->get_port,
141             Proto => 'tcp',
142             Timeout => $self->get_timeout,
143             )
144             or croak sprintf
145             'failed connect %s:%s (%s)',
146             $self->get_host,
147             $self->get_port,
148             $@,
149             );
150             }
151 0 0       0 if ( ! $self->get_socket ) {
152 0         0 croak "socket creation failed ($!)";
153             }
154 0         0 $self->get_socket()->sockopt(SO_KEEPALIVE,1);
155 0         0 $self->get_socket()->sockopt(SO_RCVTIMEO,$self->get_timeout());
156 0         0 $self->get_socket()->sockopt(SO_SNDTIMEO,$self->get_timeout());
157 0         0 return $self;
158             }
159              
160             =head2 $api->login()
161              
162             Connect happens on construction if you provide host address, username and password
163              
164             my $api = MikroTik::API->new({ host => 'mikrotik.example.org' });
165              
166             $api->set_username('whoami');
167             $api->set_password('SECRET');
168              
169             $api->login();
170              
171             =cut
172              
173             sub login {
174 0     0 1 0 my ( $self ) = @_;
175              
176 0 0 0     0 if ( ! $self->get_username() && defined( $self->get_password() ) ) {
177 0         0 croak 'username and password must be set before connect()';
178             }
179 0 0       0 if ( ! $self->get_socket() ) {
180 0         0 $self->connect();
181             }
182              
183             # RouterOS post v6.43 has new authentication method, thus pushing login/pass in the very first request.
184 0         0 my @command = ('/login');
185 0         0 push( @command, '=name=' . $self->get_username() );
186 0         0 push( @command, '=password=' . $self->get_password() );
187 0         0 my ( $retval, @results ) = $self->talk( \@command );
188 0 0       0 croak 'disconnected while logging in' if !defined $retval;
189 0 0       0 if ( $retval > 1 ) {
190 0         0 croak 'error during establishing login: ' . $results[0]{'message'};
191             }
192              
193             # if we got "=ret=" in response - then assuming this is old style AUTH
194 0 0       0 if ( exists $results[0]{'ret'} ) {
195 0         0 my $challenge = pack("H*",$results[0]{'ret'});
196 0         0 my $md5 = Digest::MD5->new();
197 0         0 $md5->add( chr(0) );
198 0         0 $md5->add( $self->get_password() );
199 0         0 $md5->add( $challenge );
200              
201 0         0 @command = ('/login');
202 0         0 push( @command, '=name=' . $self->get_username() );
203 0         0 push( @command, '=response=00' . $md5->hexdigest() );
204 0         0 ( $retval, @results ) = $self->talk( \@command );
205             }
206 0 0       0 croak 'disconnected while logging in' if !defined $retval;
207 0 0       0 if ( $retval > 1 ) {
208 0         0 croak 'error during establishing login: ' . $results[0]{'message'};
209             }
210              
211 0 0       0 if ( $self->get_debug() > 0 ) {
212 0         0 print 'Logged in to '. $self->get_host() .' as '. $self->get_username() ."\n";
213             }
214 0         0 return $self;
215             }
216              
217             =head2 $api->logout()
218              
219             $api->logout();
220              
221             =cut
222              
223             sub logout {
224 0     0 1 0 my ($self) = @_;
225 0         0 $self->get_socket()->close();
226 0         0 $self->set_socket( undef );
227             }
228              
229             =head2 $api->cmd( $command, \%attributes )
230              
231             # Set with no key required
232             # /system identity set name=MyNewMikroTik
233             my $returnvalue = $api->cmd( '/system/identity/set', { 'name' => 'MyNewMikroTik' } );
234             print "Name set\n" if ($returnvalue < 2);
235              
236             # Set keyed on the name "local"
237             # /interface bridge set local fast-forward=no
238             my $returnvalue = $api->cmd( '/interface/bridge/set', { '.id' => 'local', 'fast-forward' => 'no' } );
239             print "Bridge fast-forward turned off\n" if ($returnvalue < 2);
240              
241             # Set keyed on internal key
242             # /interface bridge set *cc fast-forward=no
243             my $returnvalue = $api->cmd( '/interface/bridge/set', { '.id' => '*cc', 'fast-forward' => 'no' } );
244             print "Bridge fast-forward turned off\n" if ($returnvalue < 2);
245              
246             # Reset a value
247             # /routing bgp peer set testpeer !keepalive-time
248             my $returnvalue = $api->cmd( '/routing/bgp/peer/set', { '.id' => 'testpeer', 'keepalive-time' => undef } );
249             print "Reset keepalive-time on testpeer\n" if ($returnvalue < 2);
250              
251             =cut
252              
253             sub cmd {
254 0     0 1 0 my ( $self, $cmd, $attrs_href ) = @_;
255 0         0 my @command = ($cmd);
256              
257 0         0 foreach my $attr ( keys %{$attrs_href} ) {
  0         0  
258 0 0       0 if (defined($attrs_href->{$attr})) {
259 0         0 push( @command, '='. $attr .'='. $attrs_href->{$attr} );
260             } else {
261 0         0 push( @command, '=!'. $attr );
262             }
263             }
264 0         0 my ( $retval, @results ) = $self->talk( \@command );
265              
266 0         0 return ( $retval, @results );
267             }
268              
269             =head2 $api->query( $command, \%attributes, \%conditions )
270              
271             # Get all interfaces of type ether
272             my ( $ret_interface_print, @interfaces ) = $api->query('/interface/print', { '.proplist' => '.id,name' }, { type => 'ether' } );
273             foreach my $interface ( @interfaces ) {
274             print "$interface->{name}\n";
275             }
276              
277             # get all default routes that don't have the dynamic attribute
278             my ( $ret_route_print, @routes ) = $api->query('/ip/route', { '.proplist' => '.id,dst-address' }, { 'dst-address' => '0.0.0.0/0', 'dynamic'=>undef } );
279             foreach my $route ( @routes ) {
280             print "$route->{'dst-address'}\n";
281             }
282              
283             # get all default routes that don't have the dynamic attribute (alternate using array ref)
284             my ( $ret_route_print, @routes ) = $api->query('/ip/route', { '.proplist' => '.id,dst-address' }, [ 'dst-address=0.0.0.0/0', '-dynamic' ] );
285             foreach my $route ( @routes ) {
286             print "$route->{'dst-address'}\n";
287             }
288              
289             # get all default routes along with those with the dynamic attribute (note 'or' operator as last arg)
290             my ( $ret_route_print, @routes ) = $api->query('/ip/route', { '.proplist' => '.id,dst-address' }, [ 'dst-address=0.0.0.0/0', 'dynamic', '#|' ] );
291             foreach my $route ( @routes ) {
292             print "$route->{'dst-address'}\n";
293             }
294              
295             =cut
296              
297             sub query {
298 0     0 1 0 my ( $self, $cmd, $attrs_href, $queries_ref ) = @_;
299              
300 0         0 my @command = ($cmd);
301 0         0 foreach my $attr ( keys %{$attrs_href} ) {
  0         0  
302 0 0       0 push( @command, '='. $attr .'='. (defined($attrs_href->{$attr}) ? $attrs_href->{$attr} : ''));
303             }
304 0 0       0 if (defined($queries_ref)) {
305 0 0       0 if (ref($queries_ref) eq 'HASH') {
    0          
306 0         0 foreach my $query (keys %{$queries_ref} ) {
  0         0  
307 0 0       0 if (defined($queries_ref->{$query})) {
308 0         0 push( @command, '?'. $query .'='. $queries_ref->{$query} );
309             } else {
310 0         0 push( @command, '?-'. $query);
311             }
312             }
313             } elsif (ref($queries_ref) eq 'ARRAY') {
314 0         0 foreach my $query (@{$queries_ref} ) {
  0         0  
315 0         0 push( @command, '?'. $query);
316             }
317             }
318             }
319 0         0 my ( $retval, @results ) = $self->talk( \@command );
320              
321 0         0 return ( $retval, @results );
322             }
323              
324             =head2 $api->get_by_key( $command, $keycolumn )
325              
326             my %interface = $api->get_by_key('/interface/ethernet/print', 'name' );
327             print "$interface{'ether1'}->{running}\n";
328              
329             =cut
330              
331             sub get_by_key {
332 0     0 1 0 my ( $self, $cmd, $id ) = @_;
333 0   0     0 $id ||= '.id';
334 0         0 my @command = ($cmd);
335 0         0 my %ids;
336 0         0 my ( $retval, @results ) = $self->talk( \@command );
337 0 0       0 croak 'disconnected' if !defined $retval;
338 0 0       0 if ($retval > 1) {
339 0         0 croak $results[0]{'message'};
340             }
341 0         0 foreach my $attrs ( @results ) {
342 0         0 my $key = '';
343 0         0 foreach my $attr ( keys %{ $attrs } ) {
  0         0  
344 0         0 my $val = $attrs->{$attr};
345 0 0       0 if ($attr eq $id) {
346 0         0 $key = $val;
347             }
348             }
349 0 0       0 if ( $key ) {
350 0         0 $ids{$key} = $attrs;
351             }
352             }
353 0         0 return %ids;
354             }
355              
356             =head1 ACCESSORS
357              
358             =head2 $api->get_host(), $api->set_host( $hostname )
359              
360             =cut
361              
362             has 'host' => ( is => 'rw', reader => 'get_host', writer => 'set_host', isa => Str );
363              
364             =head2 $api->get_port(), $api->set_port( $portnumber )
365              
366             =cut
367              
368             has 'port' => ( is => 'ro', reader => '_get_port', writer => 'set_port', isa => Int );
369              
370             =head2 $api->get_username(), $api->set_username( $username )
371              
372             =cut
373              
374             has 'username' => ( is => 'rw', reader => 'get_username', writer => 'set_username', isa => Str );
375              
376             =head2 $api->get_password(), $api->set_password( $password )
377              
378             =cut
379              
380             has 'password' => ( is => 'rw', reader => 'get_password', writer => 'set_password', isa => Str );
381              
382             =head2 $api->get_use_ssl(), $api->set_use_ssl( $zero_or_one )
383              
384             =cut
385              
386             has 'use_ssl' => ( is => 'rw', reader => 'get_use_ssl', writer => 'set_use_ssl', isa => Bool );
387              
388             =head2 $api->get_ssl_verify(), $api->set_ssl_verify( $zero_or_one )
389              
390             =cut
391              
392             has 'ssl_verify' => ( is => 'rw', reader => 'get_ssl_verify', writer => 'set_ssl_verify', isa => Int, default => 1 );
393              
394             =head2 $api->get_new_auth_method(), $api->set_new_auth_method( $zero_or_one )
395              
396             DEPRECATED: does not have any effect any longer. Login looks up wether new method is possible and falls back to old method. This parameter will be removed in future.
397             Auth method changed in RouterOS v6.43+ (https://wiki.mikrotik.com/wiki/Manual:API#Initial_login) and reduces login by one call but sends password in plaintext.
398              
399             =cut
400              
401             has 'new_auth_method' => ( is => 'rw', reader => 'get_new_auth_method', writer => 'set_new_auth_method', isa => Bool, default => 0 );
402              
403             =head2 $api->get_autoconnect(), $api->set_autoconnect( $zero_or_one )
404              
405             =cut
406              
407             has 'autoconnect' => ( is => 'rw', reader => 'get_autoconnect', writer => 'set_autoconnect', isa => Bool, default => 1 );
408              
409             =head2 $api->get_socket(), $api->set_socket( $io_socket )
410              
411             If you need to use an existing socket for the API connection.
412              
413             my $socket = IO::Socket::INET->new();
414             $api->set_socket( $socket );
415              
416             =cut
417              
418             sub _MaybeIOSocket {
419             return Type::Tiny->new(
420             name => '_MaybeIOSocket',
421 0 0   0   0 constraint => sub { defined $_ ? $_->isa('IO::Socket') : 1 },
422 0     0   0 message => sub { "$_ ain't an IO::Socket or undef" },
423 1     1   14 );
424             }
425             has 'socket' => ( is => 'rw', reader => 'get_socket', writer => 'set_socket', isa => _MaybeIOSocket );
426              
427             =head2 $api->get_debug(), $api->set_debug( $int )
428              
429             $api->set_debug(0); # no debug
430             $api->set_debug(5); # verbose debug to STDOUT
431              
432             =cut
433              
434             has 'debug' => ( is => 'rw', reader => 'get_debug', writer => 'set_debug', isa => Int, default => 0 );
435              
436             =head2 $api->get_timeout(), $api->set_timeout( $seconds )
437              
438             Abort connect after $seconds of no reply from MikroTik. This _will not_ affect lost connections. Use probe_before_talk for this.
439              
440             =cut
441              
442             has 'timeout' => ( is => 'rw', reader => 'get_timeout', writer => 'set_timeout', isa => Int, default => 5 );
443              
444             =head2 $api->get_probe_before_talk(), $api->set_probe_before_talk( $seconds )
445              
446             Use this attribute to enable a test command with timeout to ensure that the connection is still alive before sending the actual command.
447             This is very useful for long lasting connections that may get disconnected while idling. A broken connection will not be recognized otherwise,
448             because the socket still exists and the command will last forever. The advantage over a common timeout for all commands is that long lasting
449             commands are still possible. Set this to 0 if you use many consequent commands and reenable it after completion.
450              
451             $api->set_probe_before_talk(0); # no probing of connection before sending command and read reply
452             $api->set_probe_before_talk(5); # a simple command will be sent and after 5 seconds of no reply, the connection is assumed as broken
453              
454             =cut
455              
456             has 'probe_before_talk' => ( is => 'rw', reader => 'get_probe_before_talk', writer => 'set_probe_before_talk', isa => Int, default => 0 );
457              
458             =head2 $api->get_reconnect_after_failed_probe(), $api->set_reconnect_after_failed_probe( $zero_or_one )
459              
460             If connection is recognized as broken then either reconnect or die otherwise.
461              
462             =cut
463              
464             has 'reconnect_after_failed_probe' => ( is => 'rw', reader => 'get_reconnect_after_failed_probe', writer => 'set_reconnect_after_failed_probe', isa => Bool, default => 1 );
465              
466             =head1 SEMI-PUBLIC METHODS
467              
468             can be useful for advanced users, but too complex for daily use
469              
470             =head2 $api->talk( \@sentence )
471              
472             =cut
473              
474             # Send a sentence (command) and then read sentences back until we
475             # get a '!done' sentence
476             # returns 1 on success or 2 on trap, 3 on fatal
477             # along with an array of hashref for the results
478             sub talk {
479 0     0 1   my ( $self, $sentence_aref ) = @_;
480              
481 0 0         if( $self->get_probe_before_talk() ) {
482 0           my $seconds = $self->get_probe_before_talk();
483 0           $self->set_probe_before_talk(0);
484             timeout $seconds => sub {
485 0     0     $self->talk( ['/login'] );
486 0           };
487 0           $self->set_probe_before_talk($seconds);
488 0 0         if( $@ ) {
489 0 0         if( $self->get_reconnect_after_failed_probe() ) {
490 0           $self->connect();
491 0           $self->login();
492             }
493             else {
494 0           return( 3, {message => 'could not talk to MikroTik'});
495             }
496             }
497             }
498              
499 0           $self->_write_sentence( $sentence_aref );
500 0           my ( @reply, @attrs );
501 0           my $retval;
502              
503             # Keep reading sentences until we get one that begins with '!done'
504             # Put each sentence into a hashref
505 0           while ( @reply = $self->_read_sentence()) {
506 0 0         if ($reply[0] eq '!done') {
    0          
    0          
    0          
507 0   0       $retval //= 1; # Set this if it has not already been set
508             } elsif ($reply[0] eq '!re') {
509 0           $retval = 1;
510             } elsif ($reply[0] eq '!trap') {
511 0           $retval = 2;
512             } elsif ($reply[0] eq '!fatal') {
513 0           $retval = 3;
514             }
515 0           my %dataset;
516 0           foreach my $line ( @reply ) {
517             # Only consider words of the form "=var=value"
518 0 0         if ( my ($key, $value) = ( $line =~ /^=([^=]+)=(.*)/s ) ) {
519 0           $dataset{$key} = $value;
520             }
521             }
522 0 0         push( @attrs, \%dataset ) if (keys %dataset);
523 0 0         last if ($reply[0] eq '!done');
524             }
525 0 0         if (!@reply) {
526             # network error
527 0           return( 3, {message => 'disconnected'});
528             }
529 0           return ( $retval, @attrs );
530             }
531              
532             =head2 $api->raw_talk( \@sentence )
533              
534             =cut
535              
536             sub raw_talk {
537 0     0 1   my ( $self, $sentence_aref ) = @_;
538              
539 0           $self->_write_sentence( $sentence_aref );
540 0           my ( @reply, @response );
541 0           my $retval;
542              
543 0           while ( @reply = $self->_read_sentence() ) {
544 0 0         if ($reply[0] eq '!done') {
    0          
    0          
    0          
545 0   0       $retval //= 0;
546             } elsif ($reply[0] eq '!re') {
547 0           $retval = 1;
548             } elsif ($reply[0] eq '!trap') {
549 0           $retval = 2;
550             } elsif ($reply[0] eq '!fatal') {
551 0           $retval = 3;
552             }
553 0           foreach my $line ( @reply ) {
554 0           push ( @response, $line );
555             }
556 0 0         last if ($reply[0] eq '!done');
557             }
558 0           return ( $retval, @response );
559             }
560              
561             ### ACCESSORS (overrridden extended functionality)
562              
563             sub get_port {
564 0     0 1   my ( $self ) = @_;
565 0 0         $self->_get_port()
    0          
566             ? $self->_get_port()
567             : $self->get_use_ssl()
568             ? 8729
569             : 8728
570             ;
571             }
572              
573             ### INTERNAL METHODS
574              
575             sub _write_sentence {
576 0     0     my ( $self, $sentence_aref ) = @_;
577              
578 0           foreach my $word ( @{$sentence_aref} ) {
  0            
579 0           $self->_write_word( $word );
580 0 0         if ( $self->get_debug() > 2 ) {
581 0           print ">>> $word\n";
582             }
583             }
584 0           $self->_write_word('');
585             }
586              
587             sub _write_word {
588 0     0     my ( $self, $word ) = @_;
589 0           $self->_write_len( length $word );
590 0           my $socket = $self->get_socket();
591 0           print $socket $word;
592             }
593              
594             sub _write_len {
595 0     0     my ( $self, $len ) = @_;
596              
597 0           my $socket = $self->get_socket();
598 0 0         if ( $len < 0x80 ) {
    0          
    0          
    0          
599 0           print $socket chr($len);
600             }
601             elsif ($len < 0x4000) {
602 0           $len |= 0x8000;
603 0           print $socket chr(($len >> 8) & 0xFF);
604 0           print $socket chr($len & 0xFF);
605             }
606             elsif ($len < 0x200000) {
607 0           $len |= 0xC00000;
608 0           print $socket chr(($len >> 16) & 0xFF);
609 0           print $socket chr(($len >> 8) & 0xFF);
610 0           print $socket chr($len & 0xFF);
611             }
612             elsif ($len < 0x10000000) {
613 0           $len |= 0xE0000000;
614 0           print $socket chr(($len >> 24) & 0xFF);
615 0           print $socket chr(($len >> 16) & 0xFF);
616 0           print $socket chr(($len >> 8) & 0xFF);
617 0           print $socket chr($len & 0xFF);
618             }
619             else {
620 0           print $socket chr(0xF0);
621 0           print $socket chr(($len >> 24) & 0xFF);
622 0           print $socket chr(($len >> 16) & 0xFF);
623 0           print $socket chr(($len >> 8) & 0xFF);
624 0           print $socket chr($len & 0xFF);
625             }
626             }
627              
628             # Read words until we get a word with zero length. All sentences
629             # begin with a word that begins with '!'
630             # Returns an array of words
631             sub _read_sentence {
632 0     0     my ( $self ) = @_;
633              
634 0           my ( @reply );
635              
636 0           my $word = $self->_read_word();
637 0 0         return if (!$word);
638              
639 0 0         croak "Protocol error (sentence word does being with \"!\"\n" if ($word !~ /^!/);
640              
641 0           do {
642 0           push( @reply, $word );
643 0 0         if ( $self->get_debug() > 2 ) {
644 0           print "<<< $word\n"
645             }
646             } while ( $word = $self->_read_word() );
647 0           return (@reply );
648             }
649              
650             # Read a word from the Mikrotik
651             # Return the word read (maybe zero length string), or undef on EOF or error
652             sub _read_word {
653 0     0     my ( $self ) = @_;
654              
655 0           my $ret_line = '';
656 0           my $len = eval { $self->_read_len(); }; # catch EOF
  0            
657 0 0         return if !defined($len);
658 0 0         if ( $len > 0 ) {
659 0 0         if ( $self->get_debug() > 3 ) {
660 0           print "recv $len\n";
661             }
662 0           my $length_received = 0;
663 0           while ( $length_received < $len ) {
664 0           my $line = '';
665 0           $self->get_socket()->read( $line, $len );
666 0 0 0       last if !defined($line) || $line eq ''; # EOF
667 0           $ret_line .= $line; # append to $ret_line, in case we didn't get the whole word and are going round again
668 0           $length_received += length $line;
669             }
670 0 0         return if length($ret_line) != $len; # EOF or a protocol error
671             }
672 0           return $ret_line;
673             }
674              
675             # Read the length of the next word
676             sub _read_len {
677 0     0     my ( $self ) = @_;
678              
679 0 0         if ( $self->get_debug() > 4 ) {
680 0           print "start read_len\n";
681             }
682              
683 0           my $len = $self->_read_byte();
684              
685 0 0         if ( ($len & 0x80) == 0x00 ) {
    0          
    0          
    0          
    0          
686 0 0         if ( $self->get_debug() > 4 ) {
687 0           print "read_len got $len\n";
688             }
689 0           return $len
690             }
691             elsif ( ($len & 0xC0) == 0x80 ) {
692 0           $len &= ~0x80;
693 0           $len <<= 8;
694 0           $len += $self->_read_byte();
695             }
696             elsif ( ($len & 0xE0) == 0xC0 ) {
697 0           $len &= ~0xC0;
698 0           $len <<= 8;
699 0           $len += $self->_read_byte();
700 0           $len <<= 8;
701 0           $len += $self->_read_byte();
702             }
703             elsif ( ($len & 0xF0) == 0xE0 ) {
704 0           $len &= ~0xE0;
705 0           $len <<= 8;
706 0           $len += $self->_read_byte();
707 0           $len <<= 8;
708 0           $len += $self->_read_byte();
709 0           $len <<= 8;
710 0           $len += $self->_read_byte();
711             }
712             elsif ( ($len & 0xF8) == 0xF0 ) {
713 0           $len = $self->_read_byte();
714 0           $len <<= 8;
715 0           $len += $self->_read_byte();
716 0           $len <<= 8;
717 0           $len += $self->_read_byte();
718 0           $len <<= 8;
719 0           $len += $self->_read_byte();
720             }
721              
722 0 0         if ( $self->get_debug() > 4 ) {
723 0           print "read_len got $len\n";
724             }
725              
726 0           return $len;
727             }
728              
729             sub _read_byte{
730 0     0     my ( $self ) = @_;
731 0           my $line = '';
732 0           $self->get_socket()->read( $line, 1 );
733 0 0 0       croak 'EOF' if !defined($line) || length($line) != 1;
734 0           return ord($line);
735             }
736              
737             =head1 ABOUT
738              
739             =head2 Contributors
740              
741             Object-Orientated Rebuild of prior contributions, based on:
742              
743             =over 4
744              
745             =item *
746              
747             initial release from cheesegrits in MikroTik forum: http://forum.mikrotik.com/viewtopic.php?p=108530#p108530
748              
749             =item *
750              
751             added C and fixes by elcamlost: https://github.com/elcamlost/mikrotik-perl-api/commit/10e5da1fd0ccb4a249ed3047c1d22c97251f666e
752              
753             =item *
754              
755             SSL support by akschu: https://github.com/akschu/MikroTikPerl/commit/9b689a7d7511a1639ffa2118c8e549b5cec1290d
756              
757             =item *
758              
759             upgrade to v2.0.0 by Steffen Winkler
760              
761             =back
762              
763             =head2 Design decisions
764              
765             =over 4
766              
767             =item *
768              
769             Use of Moo for OO
770              
771             =item *
772              
773             higher compilation time of Moo based lib negligible because of slow I/O operations
774              
775             =item *
776              
777             Change from Moose to Moo with version 2.0.0 because of XS dependencies of Moose
778              
779             =back
780              
781             =head1 AUTHOR
782              
783             Martin Gojowsky, C
784              
785             =head1 BUGS
786              
787             Please report any bugs or feature requests to C, or through
788             the web interface at L. I will be notified, and then you'll
789             automatically be notified of progress on your bug as I make changes.
790              
791             =head2 Known issues
792              
793             =over 4
794              
795             =item *
796              
797             Quite high compile time because of using Moo. Use of a persistent running framework recommended.
798              
799             =back
800              
801             =head1 TODO
802              
803             =over 4
804              
805             =item *
806              
807             Add a parameter talk_timeout as an alternative for probe_before_talk that enables an actual timeout for each command.
808              
809             =back
810              
811             =head1 SUPPORT
812              
813             You can find documentation for this module with the perldoc command.
814              
815             perldoc MikroTik::API
816              
817             You can also look for information at:
818              
819             =over 4
820              
821             =item * RT: CPAN's request tracker (report bugs here)
822              
823             L
824              
825             =item * AnnoCPAN: Annotated CPAN documentation
826              
827             L
828              
829             =item * CPAN Ratings
830              
831             L
832              
833             =item * Search CPAN
834              
835             L
836              
837             =back
838              
839              
840             =head1 ACKNOWLEDGEMENTS
841              
842              
843             =head1 LICENSE AND COPYRIGHT
844              
845             Copyright 2015 Martin Gojowsky.
846              
847             This program is distributed under the MIT (X11) License:
848             L
849              
850             Permission is hereby granted, free of charge, to any person
851             obtaining a copy of this software and associated documentation
852             files (the "Software"), to deal in the Software without
853             restriction, including without limitation the rights to use,
854             copy, modify, merge, publish, distribute, sublicense, and/or sell
855             copies of the Software, and to permit persons to whom the
856             Software is furnished to do so, subject to the following
857             conditions:
858              
859             The above copyright notice and this permission notice shall be
860             included in all copies or substantial portions of the Software.
861              
862             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
863             EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
864             OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
865             NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
866             HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
867             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
868             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
869             OTHER DEALINGS IN THE SOFTWARE.
870              
871              
872             =cut
873              
874             1; # End of MikroTik::API