File Coverage

blib/lib/NetSDS/App/SMPP.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             #===============================================================================
2             #
3             # FILE: SMPP.pm
4             #
5             # DESCRIPTION: Flexible SMPP server application framework
6             #
7             # NOTES: Based on NetSDS::App, Net::SMPP and IO::Select
8             # AUTHOR: Michael Bochkaryov (RATTLER), <misha@rattler.kiev.ua>
9             # COMPANY: Net.Style
10             # VERSION: 1.0
11             # CREATED: 23.10.2008 15:18:46 EEST
12             #===============================================================================
13              
14             =head1 NAME
15              
16             NetSDS::App::SMPP - SMPP application superclass
17              
18             =head1 SYNOPSIS
19              
20             package SMPPServer;
21              
22             use base qw(NetSDS::App::SMPP);
23              
24             exit 1;
25              
26             =head1 DESCRIPTION
27              
28             C<NetSDS> module contains superclass all other classes should be inherited from.
29              
30             =cut
31              
32             package NetSDS::App::SMPP;
33              
34 2     2   31154 use 5.8.0;
  2         7  
  2         97  
35 2     2   11 use strict;
  2         3  
  2         75  
36 2     2   16 use warnings;
  2         9  
  2         64  
37              
38 2     2   1406 use Errno qw(:POSIX);
  2         1150  
  2         1088  
39              
40 2     2   3644 use Net::SMPP;
  2         160446  
  2         30  
41 2     2   1735 use IO::Socket::INET;
  2         4  
  2         17  
42 2     2   5062 use IO::Select;
  2         3681  
  2         105  
43              
44 2     2   850 use NetSDS::Util::String;
  0            
  0            
45             use NetSDS::Util::Convert;
46              
47             use IPC::ShareLite;
48             use JSON;
49              
50             use base qw(NetSDS::App);
51              
52             use version; our $VERSION = "1.200";
53              
54             # Default listen IP address and TCP port
55             use constant DEFAULT_BIND_ADDR => '127.0.0.1';
56             use constant DEFAULT_LISTEN_PORT => '9900';
57             use constant SYSTEM_NAME => 'NETSDS';
58              
59             # SMPP PDU command_id table
60             use constant cmd_tab => {
61             0x80000000 => 'generic_nack',
62             0x00000001 => 'bind_receiver',
63             0x80000001 => 'bind_receiver_resp',
64             0x00000002 => 'bind_transmitter',
65             0x80000002 => 'bind_transmitter_resp',
66             0x00000003 => 'query_sm',
67             0x80000003 => 'query_sm_resp',
68             0x00000004 => 'submit_sm',
69             0x80000004 => 'submit_sm_resp',
70             0x80000005 => 'deliver_sm_resp',
71             0x00000006 => 'unbind',
72             0x80000006 => 'unbind_resp',
73             0x00000007 => 'replace_sm',
74             0x80000007 => 'replace_sm_resp',
75             0x00000008 => 'cancel_sm',
76             0x80000008 => 'cancel_sm_resp',
77             0x00000009 => 'bind_transceiver',
78             0x80000009 => 'bind_transceiver_resp',
79             0x0000000b => 'outbind',
80             0x00000015 => 'enquire_link',
81             0x80000015 => 'enquire_link_resp',
82             };
83              
84             #===============================================================================
85             #
86              
87             =head1 CLASS METHODS
88              
89             =over
90              
91             =item B<new([...])>
92              
93             Constructor
94              
95             my $object = NetSDS::SomeClass->new(%options);
96              
97             =cut
98              
99             #-----------------------------------------------------------------------
100              
101             __PACKAGE__->mk_accessors('listener'); # Listening socket
102             __PACKAGE__->mk_accessors('in_queue'); # Queue socket for incoming events (MT)
103             __PACKAGE__->mk_accessors('out_queue'); # Queue socket for outgoing events (MO, DLR)
104             __PACKAGE__->mk_accessors('selector'); # IO::Select handler
105             __PACKAGE__->mk_accessors('handlers'); # SMPP sessions handlers
106             __PACKAGE__->mk_accessors('shm'); # Shared memory interconnection area
107              
108             sub initialize {
109              
110             my ( $this, %params ) = @_;
111              
112             # Common application initialization
113             $this->SUPER::initialize(%params);
114              
115             # Initialize signals processing
116             $this->set_signal_processors();
117              
118             # Create select() handler for incoming events
119             $this->selector( IO::Select->new() );
120              
121             # Initialize queue listener for outgoing events
122             $this->_init_out_queue();
123             $this->selector->add( $this->out_queue );
124              
125             # Initialize queue for incoming (MT) events
126             $this->_init_in_queue();
127              
128             # Initialize listening socket and add to select()
129             $this->_init_listener();
130             $this->selector->add( $this->listener );
131              
132             # Set initial empty array of handlers hashref
133             $this->handlers( {} );
134              
135             # Initialize SHM area
136             $this->_init_shm();
137              
138             } ## end sub initialize
139              
140             sub _init_shm {
141              
142             my ($this) = @_;
143              
144             # Create SHM segment for data exchange between
145             # SMPP server and Queue processor
146             my $shm = new IPC::ShareLite(
147             -key => $this->conf->{shm}->{segment},
148             -create => 'yes',
149             -destroy => 'yes'
150             );
151              
152             if ( !$shm ) {
153             $this->log( "error", "Cant create shared memory segment" );
154             $this->speak("Cant create shared memory segment");
155             die $!;
156             }
157              
158             # Initialize shared memory clients list
159             # Structure: hash reference with system_id => 1
160             $shm->store( encode_json( {} ) );
161              
162             $this->shm($shm);
163              
164             } ## end sub _init_shm
165              
166             sub set_signal_processors {
167              
168             my ( $this, %params ) = @_;
169              
170             #$SIG{CHLD} = 'IGNORE';
171             #$SIG{HUP} = 'IGNORE';
172             #$SIG{TERM} = 'IGNORE';
173             #$SIG{PIPE} = sub { warn "FUCK!\n" };
174              
175             }
176              
177             sub _init_listener {
178              
179             my ( $this, %params ) = @_;
180              
181             # Get bind address and TCP port
182             my $bind_addr = DEFAULT_BIND_ADDR;
183             my $bind_port = DEFAULT_LISTEN_PORT;
184              
185             # If configuration exists, use parameters
186             if ( $this->conf and $this->conf->{smpp} ) {
187              
188             # Get bind IP address
189             if ( defined $this->conf->{smpp}->{host} ) {
190             $bind_addr = $this->conf->{smpp}->{host};
191             }
192              
193             # Get bind TCP port
194             if ( defined $this->conf->{smpp}->{port} ) {
195             $bind_port = $this->conf->{smpp}->{port};
196             }
197              
198             } else {
199             $this->speak("Oops! No configuration found!");
200             }
201              
202             # Create listening socket
203             $this->listener(
204             Net::SMPP->new_listen(
205             $bind_addr,
206             port => $bind_port,
207             smpp_version => 0x34,
208             interface_version => 0x00,
209             addr_ton => 0x00,
210             addr_npi => 0x01,
211             source_addr_ton => 0x00,
212             source_addr_npi => 0x01,
213             dest_addr_ton => 0x00,
214             dest_addr_npi => 0x01,
215             system_type => SYSTEM_NAME,
216             facilities_mask => 0x00010003,
217             )
218             );
219              
220             # If cant listen, die with error message
221             if ( !$this->listener() ) {
222             $this->log( 'error', "Cant open listening TCP socket on port $bind_port" );
223             die("ERROR! Cant open listening TCP socket on port $bind_port. Closing application!\n");
224             } else {
225             $this->log( "info", "Listening on TCP port $bind_port" );
226             }
227              
228             } ## end sub _init_listener
229              
230             sub _init_in_queue {
231              
232             my ( $this, %params ) = @_;
233              
234             $this->in_queue(
235             NetSDS::Queue->new(
236             server => '127.0.0.1:22201',
237             )
238             );
239              
240             }
241              
242             sub _init_out_queue {
243              
244             my ( $this, %params ) = @_;
245              
246             $this->out_queue(
247             IO::Socket::INET->new(
248             PeerAddr => '127.0.0.1',
249             PeerPort => '9999',
250             Proto => 'tcp',
251             )
252             );
253              
254             if ( $this->out_queue ) {
255             $this->log( "info", "Successfully connected to OUT Queue server" );
256             } else {
257             $this->log( "error", "Cant connect to ougoing queue server" );
258             die("ERROR! Cant connect to outgoing queue server");
259             }
260              
261             } ## end sub _init_out_queue
262              
263             sub main_loop {
264              
265             my ( $this, %params ) = @_;
266              
267             # Run user defined hooks on startup
268             $this->start();
269              
270             # Run main process loop
271             while ( !$this->{to_finalize} ) {
272              
273             # Wait for incoming events on all sockets
274             my ( $sel_r, $sel_w, $sel_x ) = IO::Select->select( $this->selector, undef, undef );
275              
276             # Go through available for reading sockets
277             if ( $sel_r and my @readers = @{$sel_r} ) {
278              
279             # Check all sockets ready for reading
280             foreach my $reader (@readers) {
281              
282             no warnings 'uninitialized';
283              
284             if ( $reader eq $this->listener ) {
285              
286             # Process incoming connection
287             $this->_accept_incoming();
288              
289             } elsif ( $reader eq $this->out_queue ) {
290              
291             # Process ougoing queue
292             $this->_process_out_queue();
293              
294             } else {
295              
296             # Process events from established SMPP connection
297             foreach my $hdl_key ( keys %{ $this->handlers } ) {
298             if ( $this->handlers->{$hdl_key} and ( $this->handlers->{$hdl_key}->{smpp} eq $reader ) ) {
299             $this->_process_socket( $this->handlers->{$hdl_key} );
300             }
301             }
302              
303             }
304             ; ## end if ( $reader eq $this->listener )
305              
306             use warnings 'all';
307              
308             } ## end foreach my $reader (@readers)
309              
310             } ## end if ( $sel_r and my @readers...
311              
312             } ## end while ( !$this->{to_finalize...
313              
314             # Run user defined hooks on shutdown
315             $this->stop();
316              
317             } ## end sub main_loop
318              
319             #***********************************************************************
320              
321             =item B<_accept_incoming()> - accept incoming SMPP connection
322              
323             Internal method providing TCP connection accept and add new handler.
324              
325             =cut
326              
327             #-----------------------------------------------------------------------
328              
329             sub _accept_incoming {
330              
331             my ( $this, %params ) = @_;
332             $this->log( "info", "New connection arrived on SMPP socket" );
333              
334             # Try to accept incoming connection
335             if ( my $conn = $this->listener->accept() ) {
336              
337             $this->log( "info", "TCP connection accepted" );
338             $this->speak("New client accepted");
339              
340             # Add socket to IO::Select object
341             $this->selector->add($conn);
342              
343             # Determine connection key as "host:port" string
344             my $hdl_id = $conn->peerhost . ":" . $conn->peerport;
345              
346             # Create internal connection descriptor
347             my $handler = {
348             id => $hdl_id, # identifier
349             smpp => $conn, # SMPP socket
350             system_id => undef, # client system_id
351             authenticated => undef, # is authenticated
352             out_seq => 1, # sequence_id for correct responses
353             unacked => 0, # counter for unaccepted commands
354             };
355              
356             # Add descriptor to connections table
357             $this->{handlers}->{$hdl_id} = $handler;
358              
359             } else {
360             $this->log( "error", "Cant accept() incoming connection" );
361             }
362              
363             } ## end sub _accept_incoming
364              
365             sub _process_out_queue {
366             my ( $this, %params ) = @_;
367              
368             # Try to get next line from Queue server over TCP socket
369             if ( my $line = $this->out_queue->getline() ) {
370              
371             # Return if keepalive
372             if ( $line =~ '-MARK-' ) {
373             $this->speak("Keepalive from queue server");
374             return 1;
375             }
376              
377             # FIXME - provide incorrect data handling
378             my $mo = decode_json( conv_base64_str($line) );
379              
380             use Data::Dumper;
381             print Dumper($mo);
382              
383             # Check if know system_id
384             if ( $mo->{client} ) {
385             # Looking for proper ESME
386             foreach my $hdl ( values %{ $this->handlers } ) {
387             if ( $hdl->{system_id} eq $mo->{client} ) {
388             if ( ( $hdl->{mode} eq 'transceiver' ) and ( $hdl->{mode} eq 'receiver' ) ) {
389             $this->_deliver_sm( $hdl, $mo ); # send deliver_sm to ESME
390             }
391             }
392             }
393             } else {
394             $this->log( "warning", "MO event without client (system_id)" );
395             }
396              
397             } ## end if ( my $line = $this->out_queue...
398             } ## end sub _process_out_queue
399              
400             sub _deliver_sm {
401              
402             my ( $this, $hdl, $mo ) = @_;
403              
404             if ( $mo->{id} ) {
405              
406             # Set default parameters (for MO SM)
407             my $message_id = $mo->{id};
408             my $esm_class = 0x00;
409             my $source_addr_ton = 0x01;
410             my $source_addr_npi = 0x01;
411             my $source_addr = $mo->{from};
412             my $dest_addr_ton = 0x00;
413             my $dest_addr_npi = 0x01;
414             my $destination_addr = $mo->{to};
415             my $msg_text = "";
416              
417             # Set ESM class
418             if ( $mo->{dlr} ) {
419             $esm_class = 0x04; # DLR
420             $msg_text = $mo->{dlr};
421             } else {
422             $msg_text = $mo->{text}; # FIXME - here should be UDH + UD
423             }
424              
425             # Send deliver_sm
426             $hdl->{smpp}->deliver_sm(
427             source_addr_ton => $source_addr_ton, # International (MSISDN)
428             source_addr_npi => $source_addr_npi, # E.164
429             source_addr => $source_addr,
430             dest_addr_ton => $dest_addr_ton, # Unknown (default)
431             dest_addr_npi => $dest_addr_npi, # E.164
432             destination_addr => $destination_addr,
433             esm_class => $esm_class, # MO data (UDH + UD) or DLR
434             short_message => $msg_text,
435             async => 1,
436             );
437              
438             } ## end if ( $mo->{id} )
439              
440             $hdl->{out_seq}++;
441             } ## end sub _deliver_sm
442              
443             sub cmd_deliver_sm_resp {
444              
445             return 1;
446              
447             }
448              
449             sub _process_socket {
450             my ( $this, $hdl ) = @_;
451              
452             # Determine peer IP and port
453             my $peer_addr = $hdl->{smpp}->peerhost;
454             my $peer_port = $hdl->{smpp}->peerport;
455              
456             # Try to read PDU
457             my $pdu = $hdl->{smpp}->read_pdu();
458             if ( !$pdu ) {
459              
460             # Disconnect if EOF
461             if ( $hdl->{smpp}->eof() ) {
462              
463             $this->speak("EOF from ${peer_addr}:${peer_port}");
464             $this->log( "warning", "EOF from ${peer_addr}:${peer_port}" );
465              
466             # Remove socket from select()
467             $this->selector->remove( $hdl->{smpp} );
468              
469             # Close socket and remove record
470             $hdl->{smpp}->close();
471             undef $this->{handlers}->{ $hdl->{id} };
472              
473             # Update SHM struture (only for defined system_id)
474             if ( $hdl->{system_id} ) {
475             $this->shm->lock;
476             my $list = decode_json( $this->shm->fetch );
477             delete $list->{ $hdl->{system_id} };
478             $this->shm->store( encode_json($list) );
479             $this->shm->unlock;
480             }
481              
482             } else {
483              
484             $this->speak("Incoming event arrived but no SMPP PDU!");
485             $this->log( "warning", "Incoming event arrived from [${peer_addr}:${peer_port}] but no SMPP PDU!" );
486              
487             }
488              
489             } else {
490              
491             # Process incoming PDU
492             my $pdu_cmd = "unknown";
493             if ( cmd_tab->{ $pdu->{cmd} } ) {
494             $pdu_cmd = cmd_tab->{ $pdu->{cmd} };
495             }
496              
497             $this->speak("PDU arrived: $pdu_cmd");
498              
499             # Determine method name to dispatch PDU call
500             my $method_name = "cmd_" . $pdu_cmd;
501              
502             if ( $pdu_cmd eq 'enquire_link' ) {
503              
504             # process enqiure_link locally
505             $this->cmd_enquire_link( $pdu, $hdl );
506              
507             } elsif ( ( $pdu_cmd =~ /^bind_(transceiver|transmitter|receiver)/ ) and $this->can($method_name) ) {
508              
509             # Process known authentication methods
510             $this->$method_name( $pdu, $hdl );
511              
512             } elsif ( $this->can($method_name) and $hdl->{authenticated} ) {
513              
514             # process known PDUs
515             $this->$method_name( $pdu, $hdl );
516              
517             } else {
518              
519             # PDU unknown - damn it
520             $this->cmd_unknown( $pdu, $hdl );
521              
522             }
523              
524             } ## end else [ if ( !$pdu )
525              
526             } ## end sub _process_socket
527              
528             sub cmd_enquire_link {
529              
530             my ( $this, $pdu, $hdl ) = @_;
531              
532             $this->speak( "enquire_link from " . $hdl->{id} );
533              
534             my $resp = $hdl->{smpp}->enquire_link_resp(
535             seq => $pdu->{seq},
536             status => 0x00000000,
537             );
538              
539             }
540              
541             sub cmd_unknown {
542              
543             my ( $this, $pdu, $hdl ) = @_;
544              
545             $this->speak( "Uknown PDU arrived from " . $hdl->{id} );
546             $this->log( "error", "Unknown PDU received, sending generic_nack" );
547              
548             my $resp = $hdl->{smpp}->generic_nack(
549             seq => $pdu->{seq},
550             status => 0x00000003, # ESME_RINVCMDID
551             );
552              
553             }
554              
555             1;
556              
557             __END__
558              
559             =back
560              
561             =head1 EXAMPLES
562              
563              
564             =head1 BUGS
565              
566             Unknown yet
567              
568             =head1 SEE ALSO
569              
570             None
571              
572             =head1 TODO
573              
574             None
575              
576             =head1 AUTHOR
577              
578             Michael Bochkaryov <misha@rattler.kiev.ua>
579              
580             =cut
581              
582